diff --git a/.gitignore b/.gitignore index 30511684f..d49200bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ build_log.txt .logs/ .vscode/ 20*.dat + +# bird droppings from IT +.log +.loglogin diff --git a/.project b/.project deleted file mode 100644 index 5334c032e..000000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - apstools - - - - - - org.python.pydev.PyDevBuilder - - - - - - org.python.pydev.pythonNature - - diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index de3b0a124..000000000 --- a/.pydevproject +++ /dev/null @@ -1,8 +0,0 @@ - - - - /${PROJECT_DIR_NAME} - - python interpreter - bluesky - diff --git a/.travis.yml b/.travis.yml index 5ef97e2b5..4c289ada5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ python: - "3.8" # - "nightly" + # see https://github.com/BCDA-APS/apstools/issues/137#ref-commit-5c7baa2 # install: # - pip install -r requirements.txt @@ -24,7 +25,9 @@ before_script: - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - export PY_ENV=travis-$TRAVIS_PYTHON_VERSION - - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults python=$TRAVIS_PYTHON_VERSION bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd + - export MY_CONDA_CHANNELS="-c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag" + - export MY_CONDA_PACKAGES="aps-dm-api bluesky>=1.6.2 coverage databroker>=1.0.6 docopt epics-base numpy ophyd>=1.5.1 pyepics>=3.4.2 pandas requests xlrd" + - conda create -y -n $PY_ENV $MY_CONDA_CHANNELS python=$TRAVIS_PYTHON_VERSION $MY_CONDA_PACKAGES - source activate $PY_ENV - which pip - which python diff --git a/apstools/beamtime/__init__.py b/apstools/beamtime/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apstools/beamtime/apsbss.adl b/apstools/beamtime/apsbss.adl new file mode 100644 index 000000000..e86f81951 --- /dev/null +++ b/apstools/beamtime/apsbss.adl @@ -0,0 +1,668 @@ + +file { + name="/home/beams1/JEMIAN/Documents/projects/apstools/apstools/beamtime/apsbss.adl" + version=030111 +} +display { + object { + x=122 + y=83 + width=400 + height=366 + } + clr=14 + bclr=4 + cmap="" + gridSpacing=5 + gridOn=0 + snapToGrid=0 +} +"color map" { + ncolors=65 + colors { + ffffff, + ececec, + dadada, + c8c8c8, + bbbbbb, + aeaeae, + 9e9e9e, + 919191, + 858585, + 787878, + 696969, + 5a5a5a, + 464646, + 2d2d2d, + 000000, + 00d800, + 1ebb00, + 339900, + 2d7f00, + 216c00, + fd0000, + de1309, + be190b, + a01207, + 820400, + 5893ff, + 597ee1, + 4b6ec7, + 3a5eab, + 27548d, + fbf34a, + f9da3c, + eeb62b, + e19015, + cd6100, + ffb0ff, + d67fe2, + ae4ebc, + 8b1a96, + 610a75, + a4aaff, + 8793e2, + 6a73c1, + 4d52a4, + 343386, + c7bb6d, + b79d5c, + a47e3c, + 7d5627, + 58340f, + 99ffff, + 73dfff, + 4ea5f9, + 2a63e4, + 0a00b8, + ebf1b5, + d4db9d, + bbc187, + a6a462, + 8b8239, + 73ff6b, + 52da3b, + 3cb420, + 289315, + 1a7309, + } +} +text { + object { + x=10 + y=10 + width=380 + height=24 + } + "basic attribute" { + clr=14 + } + textix="APS ESAF & Proposal Info: $(P)" +} +composite { + object { + x=54 + y=39 + width=260 + height=36 + } + "composite name"="" + children { + text { + object { + x=54 + y=39 + width=120 + height=14 + } + "basic attribute" { + clr=14 + } + textix="beam line name" + align="horiz. right" + } + "text entry" { + object { + x=179 + y=39 + width=135 + height=16 + } + control { + chan="$(P)proposal:beamline" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=54 + y=59 + width=120 + height=14 + } + "basic attribute" { + clr=14 + } + textix="APS run cycle" + align="horiz. right" + } + "text entry" { + object { + x=179 + y=59 + width=135 + height=16 + } + control { + chan="$(P)esaf:cycle" + clr=14 + bclr=50 + } + limits { + } + } + } +} +"shell command" { + object { + x=19 + y=80 + width=200 + height=16 + } + command[0] { + label="update" + name="apsbss" + args="update $(P)" + } + clr=14 + bclr=51 + label=" get Proposal and ESAF info" +} +"shell command" { + object { + x=259 + y=80 + width=100 + height=16 + } + command[0] { + label="clear" + name="apsbss" + args="clear $(P)" + } + clr=14 + bclr=51 + label=" clear PVs" +} +composite { + object { + x=10 + y=101 + width=380 + height=120 + } + "composite name"="" + children { + text { + object { + x=17 + y=106 + width=120 + height=16 + } + "basic attribute" { + clr=14 + } + textix="Proposal" + } + rectangle { + object { + x=10 + y=101 + width=380 + height=120 + } + "basic attribute" { + clr=14 + fill="outline" + width=2 + } + } + text { + object { + x=20 + y=150 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="mail in" + align="horiz. right" + } + text { + object { + x=20 + y=167 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="title" + align="horiz. right" + } + "text update" { + object { + x=77 + y=167 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:title" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=184 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="users" + align="horiz. right" + } + "text update" { + object { + x=77 + y=184 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:users" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + "text update" { + object { + x=77 + y=201 + width=303 + height=12 + } + monitor { + chan="$(P)proposal:userBadges" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=201 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="badges" + align="horiz. right" + } + "choice button" { + object { + x=75 + y=151 + width=80 + height=12 + } + control { + chan="$(P)proposal:mailInFlag" + clr=14 + bclr=55 + } + stacking="column" + } + text { + object { + x=160 + y=151 + width=100 + height=12 + } + "basic attribute" { + clr=14 + } + textix="proprietary" + align="horiz. right" + } + "choice button" { + object { + x=265 + y=152 + width=80 + height=12 + } + control { + chan="$(P)proposal:proprietaryFlag" + clr=14 + bclr=55 + } + stacking="column" + } + text { + object { + x=39 + y=131 + width=30 + height=14 + } + "basic attribute" { + clr=14 + } + textix="ID" + align="horiz. right" + } + "text entry" { + object { + x=74 + y=131 + width=100 + height=14 + } + control { + chan="$(P)proposal:id" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=192 + y=132 + width=80 + height=12 + } + "basic attribute" { + clr=14 + } + textix="submitted" + align="horiz. right" + } + "text entry" { + object { + x=277 + y=132 + width=100 + height=12 + } + control { + chan="$(P)proposal:submittedDate" + clr=14 + bclr=55 + } + limits { + } + } + } +} +composite { + object { + x=10 + y=226 + width=380 + height=130 + } + "composite name"="" + children { + text { + object { + x=17 + y=231 + width=120 + height=16 + } + "basic attribute" { + clr=14 + } + textix="ESAF" + } + rectangle { + object { + x=10 + y=226 + width=380 + height=130 + } + "basic attribute" { + clr=14 + fill="outline" + width=2 + } + } + "text entry" { + object { + x=75 + y=258 + width=100 + height=14 + } + control { + chan="$(P)esaf:id" + clr=14 + bclr=50 + } + limits { + } + } + text { + object { + x=40 + y=258 + width=30 + height=14 + } + "basic attribute" { + clr=14 + } + textix="ID" + align="horiz. right" + } + "text entry" { + object { + x=247 + y=258 + width=100 + height=12 + } + control { + chan="$(P)esaf:status" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=180 + y=258 + width=61 + height=12 + } + "basic attribute" { + clr=14 + } + textix="status" + align="horiz. right" + } + text { + object { + x=20 + y=277 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="dates" + align="horiz. right" + } + "text entry" { + object { + x=75 + y=277 + width=101 + height=12 + } + control { + chan="$(P)esaf:startDate" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=181 + y=277 + width=20 + height=12 + } + "basic attribute" { + clr=14 + } + textix="to" + align="horiz. right" + } + "text entry" { + object { + x=206 + y=277 + width=101 + height=12 + } + control { + chan="$(P)esaf:endDate" + clr=14 + bclr=55 + } + limits { + } + } + text { + object { + x=20 + y=294 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="title" + align="horiz. right" + } + "text update" { + object { + x=77 + y=294 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:title" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=311 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="users" + align="horiz. right" + } + "text update" { + object { + x=77 + y=311 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:users" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + "text update" { + object { + x=77 + y=328 + width=303 + height=12 + } + monitor { + chan="$(P)esaf:userBadges" + clr=14 + bclr=55 + } + format="string" + limits { + } + } + text { + object { + x=20 + y=328 + width=50 + height=12 + } + "basic attribute" { + clr=14 + } + textix="badges" + align="horiz. right" + } + } +} diff --git a/apstools/beamtime/apsbss.db b/apstools/beamtime/apsbss.db new file mode 100644 index 000000000..d82feb753 --- /dev/null +++ b/apstools/beamtime/apsbss.db @@ -0,0 +1,345 @@ +# +# file: apsbss.db +# EPICS database for information from APS ESAF & Proposal databases +# +# BSS: Beamtime Scheduling System +# +# note: this file autogenerated by make_database.py +# ./apsbss_makedb.py | tee apsbss.db +# +# Start the EPICS IOC with a command (from EPICS base): +# softIoc -m P=ioc:bss: -d apsbss.db + + +record(stringout, "$(P)esaf:cycle") + +record(waveform, "$(P)esaf:description") { + field(FTVL, "CHAR") + field(NELM, 4096) +} + +record(stringout, "$(P)esaf:endDate") + +record(stringout, "$(P)esaf:id") + +record(stringout, "$(P)esaf:status") + +record(stringout, "$(P)esaf:sector") + +record(stringout, "$(P)esaf:startDate") + +record(waveform, "$(P)esaf:title") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)esaf:userBadges") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)esaf:users") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)esaf:user1:badgeNumber") + +record(stringout, "$(P)esaf:user1:email") + +record(stringout, "$(P)esaf:user1:firstName") + +record(stringout, "$(P)esaf:user1:lastName") + +record(stringout, "$(P)esaf:user2:badgeNumber") + +record(stringout, "$(P)esaf:user2:email") + +record(stringout, "$(P)esaf:user2:firstName") + +record(stringout, "$(P)esaf:user2:lastName") + +record(stringout, "$(P)esaf:user3:badgeNumber") + +record(stringout, "$(P)esaf:user3:email") + +record(stringout, "$(P)esaf:user3:firstName") + +record(stringout, "$(P)esaf:user3:lastName") + +record(stringout, "$(P)esaf:user4:badgeNumber") + +record(stringout, "$(P)esaf:user4:email") + +record(stringout, "$(P)esaf:user4:firstName") + +record(stringout, "$(P)esaf:user4:lastName") + +record(stringout, "$(P)esaf:user5:badgeNumber") + +record(stringout, "$(P)esaf:user5:email") + +record(stringout, "$(P)esaf:user5:firstName") + +record(stringout, "$(P)esaf:user5:lastName") + +record(stringout, "$(P)esaf:user6:badgeNumber") + +record(stringout, "$(P)esaf:user6:email") + +record(stringout, "$(P)esaf:user6:firstName") + +record(stringout, "$(P)esaf:user6:lastName") + +record(stringout, "$(P)esaf:user7:badgeNumber") + +record(stringout, "$(P)esaf:user7:email") + +record(stringout, "$(P)esaf:user7:firstName") + +record(stringout, "$(P)esaf:user7:lastName") + +record(stringout, "$(P)esaf:user8:badgeNumber") + +record(stringout, "$(P)esaf:user8:email") + +record(stringout, "$(P)esaf:user8:firstName") + +record(stringout, "$(P)esaf:user8:lastName") + +record(stringout, "$(P)esaf:user9:badgeNumber") + +record(stringout, "$(P)esaf:user9:email") + +record(stringout, "$(P)esaf:user9:firstName") + +record(stringout, "$(P)esaf:user9:lastName") + +record(stringout, "$(P)proposal:beamline") + +record(bo, "$(P)proposal:mailInFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:id") + +record(bo, "$(P)proposal:proprietaryFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:submittedDate") + +record(waveform, "$(P)proposal:title") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)proposal:userBadges") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(waveform, "$(P)proposal:users") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user1:badgeNumber") + +record(stringout, "$(P)proposal:user1:email") + +record(stringout, "$(P)proposal:user1:firstName") + +record(waveform, "$(P)proposal:user1:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user1:instId") + +record(stringout, "$(P)proposal:user1:lastName") + +record(bo, "$(P)proposal:user1:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user1:userId") + +record(stringout, "$(P)proposal:user2:badgeNumber") + +record(stringout, "$(P)proposal:user2:email") + +record(stringout, "$(P)proposal:user2:firstName") + +record(waveform, "$(P)proposal:user2:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user2:instId") + +record(stringout, "$(P)proposal:user2:lastName") + +record(bo, "$(P)proposal:user2:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user2:userId") + +record(stringout, "$(P)proposal:user3:badgeNumber") + +record(stringout, "$(P)proposal:user3:email") + +record(stringout, "$(P)proposal:user3:firstName") + +record(waveform, "$(P)proposal:user3:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user3:instId") + +record(stringout, "$(P)proposal:user3:lastName") + +record(bo, "$(P)proposal:user3:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user3:userId") + +record(stringout, "$(P)proposal:user4:badgeNumber") + +record(stringout, "$(P)proposal:user4:email") + +record(stringout, "$(P)proposal:user4:firstName") + +record(waveform, "$(P)proposal:user4:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user4:instId") + +record(stringout, "$(P)proposal:user4:lastName") + +record(bo, "$(P)proposal:user4:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user4:userId") + +record(stringout, "$(P)proposal:user5:badgeNumber") + +record(stringout, "$(P)proposal:user5:email") + +record(stringout, "$(P)proposal:user5:firstName") + +record(waveform, "$(P)proposal:user5:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user5:instId") + +record(stringout, "$(P)proposal:user5:lastName") + +record(bo, "$(P)proposal:user5:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user5:userId") + +record(stringout, "$(P)proposal:user6:badgeNumber") + +record(stringout, "$(P)proposal:user6:email") + +record(stringout, "$(P)proposal:user6:firstName") + +record(waveform, "$(P)proposal:user6:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user6:instId") + +record(stringout, "$(P)proposal:user6:lastName") + +record(bo, "$(P)proposal:user6:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user6:userId") + +record(stringout, "$(P)proposal:user7:badgeNumber") + +record(stringout, "$(P)proposal:user7:email") + +record(stringout, "$(P)proposal:user7:firstName") + +record(waveform, "$(P)proposal:user7:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user7:instId") + +record(stringout, "$(P)proposal:user7:lastName") + +record(bo, "$(P)proposal:user7:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user7:userId") + +record(stringout, "$(P)proposal:user8:badgeNumber") + +record(stringout, "$(P)proposal:user8:email") + +record(stringout, "$(P)proposal:user8:firstName") + +record(waveform, "$(P)proposal:user8:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user8:instId") + +record(stringout, "$(P)proposal:user8:lastName") + +record(bo, "$(P)proposal:user8:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user8:userId") + +record(stringout, "$(P)proposal:user9:badgeNumber") + +record(stringout, "$(P)proposal:user9:email") + +record(stringout, "$(P)proposal:user9:firstName") + +record(waveform, "$(P)proposal:user9:institution") { + field(FTVL, "CHAR") + field(NELM, 1024) +} + +record(stringout, "$(P)proposal:user9:instId") + +record(stringout, "$(P)proposal:user9:lastName") + +record(bo, "$(P)proposal:user9:piFlag") { + field(ZNAM, "OFF") + field(ONAM, "ON") +} + +record(stringout, "$(P)proposal:user9:userId") diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py new file mode 100755 index 000000000..a263eebf9 --- /dev/null +++ b/apstools/beamtime/apsbss.py @@ -0,0 +1,710 @@ +#!/usr/bin/env python + +""" +Retrieve specific records from the APS Proposal and ESAF databases. + +This code provides the command-line application: ``apsbss`` + +.. note:: BSS: APS Beamline Scheduling System + +EXAMPLES:: + + apsbss current + apsbss esaf 226319 + apsbss proposal 66083 2020-2 9-ID-B,C + +EPICS SUPPORT + +.. autosummary:: + + ~connect_epics + ~epicsClear + ~epicsSetup + ~epicsUpdate + +APS ESAF & PROPOSAL ACCESS + +.. autosummary:: + + ~getCurrentCycle + ~getCurrentEsafs + ~getCurrentInfo + ~getCurrentProposals + ~getEsaf + ~getProposal + ~iso2datetime + ~listAllBeamlines + ~listAllRuns + ~listRecentRuns + ~printColumns + ~trim + +APPLICATION + +.. autosummary:: + + ~cmd_current + ~cmd_esaf + ~cmd_proposal + ~get_options + ~main +""" + +import datetime +import dm # APS data management library +import os +import pyRestTable +import sys +import time +import yaml + +from .apsbss_ophyd import EpicsBssDevice + + +DM_APS_DB_WEB_SERVICE_URL = "https://xraydtn01.xray.aps.anl.gov:11236" +CONNECT_TIMEOUT = 5 +POLL_INTERVAL = 0.01 + +api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL) +api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL) + +parser = None + +_cache_ = {} + + +class EpicsNotConnected(Exception): ... + +class DmRecordNotFound(Exception): ... +class EsafNotFound(DmRecordNotFound): ... +class ProposalNotFound(DmRecordNotFound): ... + + +def connect_epics(prefix): + """ + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + """ + t0 = time.time() + t_timeout = t0 + CONNECT_TIMEOUT + bss = EpicsBssDevice(prefix, name="bss") + while not bss.connected and time.time() < t_timeout: + time.sleep(POLL_INTERVAL) + # printColumns(bss.read_attrs, width=40, numColumns=2) + if not bss.connected: + raise EpicsNotConnected( + f"Did not connect with EPICS {prefix} in {CONNECT_TIMEOUT}s") + t_connect = time.time() - t0 + print(f"connected in {t_connect:.03f}s") + return bss + + +def epicsClear(prefix): + """ + Clear the EPICS database. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + """ + print(f"clear EPICS {prefix}") + bss = connect_epics(prefix) + + t0 = time.time() + bss.clear() + t_clear = time.time() - t0 + print(f"cleared in {t_clear:.03f}s") + + +def epicsUpdate(prefix): + """ + Update EPICS database instance with current ESAF & proposal info. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + """ + print(f"update EPICS {prefix}") + bss = connect_epics(prefix) + + bss.clear() + cycle = bss.esaf.aps_cycle.get() + beamline = bss.proposal.beamline_name.get() + # sector = bss.esaf.sector.get() + esaf_id = bss.esaf.esaf_id.get() + proposal_id = bss.proposal.proposal_id.get() + + if len(beamline) == 0: + raise ValueError( + f"must set beamline name in {bss.proposal.beamline_name.pvname}") + elif beamline not in listAllBeamlines(): + raise ValueError(f"{beamline} is not known") + if len(cycle) == 0: + raise ValueError( + f"must set APS cycle name in {bss.esaf.aps_cycle.pvname}") + elif cycle not in listAllRuns(): + raise ValueError(f"{cycle} is not known") + + bss.clear() + + if len(esaf_id) > 0: + esaf = getEsaf(esaf_id) + bss.esaf.description.put(esaf["description"]) + bss.esaf.title.put(esaf["esafTitle"]) + bss.esaf.esaf_status.put(esaf["esafStatus"]) + bss.esaf.end_date.put(esaf["experimentEndDate"]) + bss.esaf.start_date.put(esaf["experimentStartDate"]) + + bss.esaf.user_last_names.put( + ",".join([user["lastName"] for user in esaf["experimentUsers"]]) + ) + bss.esaf.user_badges.put( + ",".join([user["badge"] for user in esaf["experimentUsers"]]) + ) + for i, user in enumerate(esaf["experimentUsers"]): + obj = getattr(bss.esaf, f"user{i+1}") + obj.badge_number.put(user["badge"]) + obj.email.put(user["email"]) + obj.first_name.put(user["firstName"]) + obj.last_name.put(user["lastName"]) + if i == 9: + break + + if len(proposal_id) > 0: + proposal = getProposal(proposal_id, cycle, beamline) + bss.proposal.mail_in_flag.put(proposal["mailInFlag"] in ("Y", "y")) + bss.proposal.proprietary_flag.put(proposal["proprietaryFlag"] in ("Y", "y")) + bss.proposal.submitted_date.put(proposal["submittedDate"]) + bss.proposal.title.put(proposal["title"]) + + bss.proposal.user_last_names.put( + ",".join([user["lastName"] for user in proposal["experimenters"]]) + ) + bss.proposal.user_badges.put( + ",".join([user["badge"] for user in proposal["experimenters"]]) + ) + for i, user in enumerate(proposal["experimenters"]): + obj = getattr(bss.proposal, f"user{i+1}") + obj.badge_number.put(user["badge"]) + obj.email.put(user["email"]) + obj.first_name.put(user["firstName"]) + obj.last_name.put(user["lastName"]) + obj.institution.put(user["institution"]) + obj.institution_id.put(str(user["instId"])) + obj.user_id.put(str(user["id"])) + obj.pi_flag.put(user.get("piFlag") in ("Y", "y")) + if i == 9: + break + + +def epicsSetup(prefix, beamline, cycle=None): + """ + Define the beamline name and APS cycle in the EPICS database. + Connect with the EPICS database instance. + + PARAMETERS + + prefix (str): + EPICS PV prefix + beamline (str): + Name of beam line (as defined by the BSS) + cycle (str): + Name of APS run cycle (as defined by the BSS). + optional: default is current APS run cycle name. + """ + if beamline not in listAllBeamlines(): + raise ValueError(f"{beamline} is not known") + if cycle is not None and cycle not in listAllRuns(): + raise ValueError(f"{cycle} is not known") + + bss = connect_epics(prefix) + + cycle = cycle or getCurrentCycle() + sector = int(beamline.split("-")[0]) + print(f"setup EPICS {prefix} {beamline} cycle={cycle} sector={sector}") + + bss.clear() + bss.esaf.aps_cycle.put(cycle) + bss.proposal.beamline_name.put(beamline) + bss.esaf.sector.put(str(sector)) + + +def getCurrentCycle(): + """Return the name of the current APS run cycle.""" + return api_bss.getCurrentRun()["name"] + + +def getCurrentEsafs(sector): + """ + Return list of ESAFs for the current year. + + PARAMETERS + + sector (str or int): + Name of sector. If ``str``, must be in ``%02d`` format (``02``, not ``2``). + """ + if isinstance(sector, int): + sector = f"{sector:02d}" + if len(sector) == 1: + sector = "0" + sector + tNow = datetime.datetime.now() + esafs = api_esaf.listEsafs(sector=sector, year=tNow.year) + results = [] + for esaf in esafs: + if tNow < iso2datetime(esaf["experimentStartDate"]): + continue + if tNow > iso2datetime(esaf["experimentEndDate"]): + continue + results.append(esaf) + return results + + +def getCurrentInfo(beamline): + """ + From current year ESAFS, return list of ESAFs & proposals with same people. + + PARAMETERS + + beamline (str): + Name of beam line (as defined by the BSS). + """ + sector = beamline.split("-")[0] + tNow = datetime.datetime.now() + + matches = [] + for esaf in api_esaf.listEsafs(sector=sector, year=tNow.year): + print(f"ESAF {esaf['esafId']}: {esaf['esafTitle']}") + esaf_badges = [user["badge"] for user in esaf["experimentUsers"]] + for run in listRecentRuns(): + for proposal in api_bss.listProposals(beamlineName=beamline, + runName=run): + print(f"proposal {proposal['id']}: {proposal['title']}") + count = 0 + for user in proposal["experimenters"]: + if user["badge"] in esaf_badges: + count += 1 + if count > 0: + matches.append( + dict( + esaf=esaf, + proposal=proposal, + num_true=count, + num_esaf_badges=len(esaf_badges), + num_proposal_badges=len(proposal["experimenters"]), + ) + ) + return matches + + +def getCurrentProposals(beamline): + """ + Return a list of proposal ID numbers that are current. + + PARAMETERS + + beamline (str): + Name of beam line (as defined by the BSS). + """ + proposals = [] + for cycle in listRecentRuns(): + for prop in api_bss.listProposals(beamlineName=beamline, runName=cycle): + prop = dict(prop) + prop["cycle"] = cycle + proposals.append(prop) + return proposals + + +def getEsaf(esafId): + """ + Return ESAF as a dictionary. + + PARAMETERS + + esafId (int): + ESAF number + """ + try: + record = api_esaf.getEsaf(int(esafId)) + except dm.ObjectNotFound: + raise EsafNotFound(esafId) + return dict(record.data) + + +def getProposal(proposalId, cycle, beamline): + """ + Return proposal as a dictionary. + + PARAMETERS + + proposalId (str): + Proposal identification number + cycle (str): + Name of APS run cycle (as defined by the BSS) + beamline (str): + Name of beam line (as defined by the BSS) + """ + # avoid possible dm.DmException + if cycle not in listAllRuns(): + raise DmRecordNotFound(f"cycle '{cycle}' not found") + + if beamline not in listAllBeamlines(): + raise DmRecordNotFound(f"beamline '{beamline}' not found") + + try: + record = api_bss.getProposal(str(proposalId), cycle, beamline) + except dm.ObjectNotFound: + raise ProposalNotFound( + f"id={proposalId}" + f" cycle={cycle}" + f" beamline={beamline}" + ) + return dict(record.data) + + +def iso2datetime(isodate): + """ + Convert a text ISO8601 date into a ``datetime`` object. + + PARAMETERS + + isodate (str): + Date and time in ISO8601 format. (e.g.: ``2020-07-01T12:34:56.789012``) + """ + return datetime.datetime.fromisoformat(isodate) + + +def listAllBeamlines(): + """Return list (from ``dm``) of known beam line names.""" + if "beamlines" not in _cache_: + _cache_["beamlines"] = [ + entry["name"] + for entry in api_bss.listBeamlines() + ] + return _cache_["beamlines"] + + +def listAllRuns(): + """Return a list of all known cycles. Cache for repeated use.""" + if "cycles" not in _cache_: + _cache_["cycles"] = sorted([ + entry["name"] + for entry in api_bss.listRuns() + ]) + return _cache_["cycles"] + + +def listRecentRuns(quantity=6): + """ + Return a list of the 6 most recent runs (2-year period). + + PARAMETERS + + quantity (int): + number of APS run cycles to include, optional (default: 6) + """ + # 6 runs is the duration of a user proposal + tNow = datetime.datetime.now() + runs = [ + run["name"] + for run in api_bss.listRuns() + if iso2datetime(run["startTime"]) <= tNow + ] + return sorted(runs, reverse=True)[:quantity] + + +def printColumns(items, numColumns=5, width=10): + """ + Print a list of ``items`` in column order. + + PARAMETERS + + items (list(str)): + List of items to report + numColumns (int): + number of columns, optional (default: 5) + width (int): + width of each column, optional (default: 10) + """ + n = len(items) + rows = n // numColumns + if n % numColumns > 0: + rows += 1 + for base in range(0, rows): + row = [ + items[base+k*rows] + for k in range(numColumns) + if base+k*rows < n] + print("".join([f"{s:{width}s}" for s in row])) + + +def trim(text, length=40): + """ + Return a string that is no longer than ``length``. + + If a string is longer than ``length``, it is shortened + to the ``length-3`` characters, then, ``...`` is appended. + For very short length, the string is shortened to ``length`` + (and no ``...`` is appended). + + PARAMETERS + + text (str): + String, potentially longer than ``length`` + length (int): + maximum length, optional (default: 40) + """ + if length < 1: + raise ValueError(f"length must be positive, received {length}") + if length < 5: + text = text[:length] + elif len(text) > length: + text = text[:length-3] + "..." + return text + + +def get_options(): + """Handle command line arguments.""" + global parser + import argparse + from apstools._version import get_versions + + version = get_versions()['version'] + + parser = argparse.ArgumentParser( + prog=os.path.split(sys.argv[0])[-1], + description=__doc__.strip().splitlines()[0], + ) + + parser.add_argument('-v', + '--version', + action='version', + help='print version number and exit', + version=version) + + subcommand = parser.add_subparsers(dest='subcommand', title='subcommand') + + subcommand.add_parser('beamlines', help="print list of beamlines") + + p_sub = subcommand.add_parser( + 'current', + help="print current ESAF(s) and proposal(s)") + p_sub.add_argument( + 'beamlineName', + type=str, + help="Beamline name") + + p_sub = subcommand.add_parser('cycles', help="print APS run cycle names") + p_sub.add_argument( + '-f', '--full', action="store_true", + default=False, + help="full report including dates (default is compact)") + p_sub.add_argument( + '-a', '--ascending', action="store_false", + default=True, + help="full report by ascending names (default is descending)") + + p_sub = subcommand.add_parser('esaf', help="print specific ESAF") + p_sub.add_argument('esafId', type=int, help="ESAF ID number") + + p_sub = subcommand.add_parser('proposal', help="print specific proposal") + p_sub.add_argument('proposalId', type=str, help="proposal ID number") + p_sub.add_argument('cycle', type=str, help="APS run (cycle) name") + p_sub.add_argument('beamlineName', type=str, help="Beamline name") + + p_sub = subcommand.add_parser('clear', help="EPICS PVs: clear") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + + p_sub = subcommand.add_parser('setup', help="EPICS PVs: setup") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + p_sub.add_argument('beamlineName', type=str, help="Beamline name") + p_sub.add_argument('cycle', type=str, help="APS run (cycle) name") + + p_sub = subcommand.add_parser('update', help="EPICS PVs: update from BSS") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + + p_sub = subcommand.add_parser( + 'report', + help="EPICS PVs: report what is in the PVs") + p_sub.add_argument('prefix', type=str, help="EPICS PV prefix") + + return parser.parse_args() + +def cmd_cycles(args): + """ + Handle ``cycles`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + if args.full: + table = pyRestTable.Table() + table.labels = "cycle start end".split() + + def sorter(entry): + return entry["startTime"] + + for entry in sorted(api_bss.listRuns(), + key=sorter, + reverse=args.ascending): + table.addRow(( + entry["name"], + entry["startTime"], + entry["endTime"], )) + print(table) + else: + printColumns(listAllRuns()) + +def cmd_current(args): + """ + Handle ``current`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + records = getCurrentProposals(args.beamlineName) + if len(records) == 0: + print(f"No current proposals for {args.beamlineName}") + else: + table = pyRestTable.Table() + table.labels = "id cycle date user(s) title".split() + for item in records: + users = trim(",".join([ + user["lastName"] + for user in item["experimenters"] + ]), 20) + table.addRow(( + item["id"], + item["cycle"], + item["submittedDate"], + users, + trim(item["title"]),)) + print(f"Current Proposal(s) on {args.beamlineName}") + print() + print(table) + + sector = args.beamlineName.split("-")[0] + records = getCurrentEsafs(sector) + if len(records) == 0: + print(f"No current ESAFs for sector {sector}") + else: + table = pyRestTable.Table() + table.labels = "id status start end user(s) title".split() + for item in records: + users = trim( + ",".join([ + user["lastName"] + for user in item["experimentUsers"] + ]), + 20) + table.addRow(( + item["esafId"], + item["esafStatus"], + item["experimentStartDate"].split()[0], + item["experimentEndDate"].split()[0], + users, + trim(item["esafTitle"], 40), + )) + print(f"Current ESAF(s) on sector {sector}") + print() + print(table) + + +def cmd_esaf(args): + """ + Handle ``esaf`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + try: + esaf = getEsaf(args.esafId) + print(yaml.dump(esaf)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + +def cmd_proposal(args): + """ + Handle ``proposal`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + try: + proposal = getProposal(args.proposalId, args.cycle, args.beamlineName) + print(yaml.dump(proposal)) + except DmRecordNotFound as exc: + print(exc) + except dm.DmException as exc: + print(f"dm reported: {exc}") + + +def cmd_report(args): + """ + Handle ``report`` command. + + PARAMETERS + + args (obj): + Object returned by ``argparse`` + """ + from ..utils import object_explorer + + bss = connect_epics(args.prefix) + object_explorer(bss) + + +def main(): + """Command-line interface for ``apsbss`` program.""" + args = get_options() + if args.subcommand == "beamlines": + printColumns(listAllBeamlines(), numColumns=4, width=15) + + elif args.subcommand == "clear": + epicsClear(args.prefix) + + elif args.subcommand == "current": + cmd_current(args) + + elif args.subcommand == "cycles": + cmd_cycles(args) + + elif args.subcommand == "esaf": + cmd_esaf(args) + + elif args.subcommand == "proposal": + cmd_proposal(args) + + elif args.subcommand == "setup": + epicsSetup(args.prefix, args.beamlineName, args.cycle) + + elif args.subcommand == "update": + epicsUpdate(args.prefix) + + elif args.subcommand == "report": + cmd_report(args) + + else: + parser.print_usage() + + +if __name__ == "__main__": + main() diff --git a/apstools/beamtime/apsbss.ui b/apstools/beamtime/apsbss.ui new file mode 100644 index 000000000..89b5fb501 --- /dev/null +++ b/apstools/beamtime/apsbss.ui @@ -0,0 +1,1730 @@ + + +MainWindow + + + + 122 + 83 + 400 + 366 + + + + + +QWidget#centralWidget {background: rgba(187, 187, 187, 255);} +QPushButton::menu-indicator {image: url(none.png); width: 0} + + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + APS ESAF & Proposal Info: $(P) + + + ESimpleLabel::WidthAndHeight + + + + 10 + 10 + 380 + 24 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 54 + 39 + 262 + 38 + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + beam line name + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 0 + 0 + 120 + 14 + + + + + + + 125 + 0 + 135 + 16 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:beamline + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + APS run cycle + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 0 + 20 + 120 + 14 + + + + + + + 125 + 20 + 135 + 16 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:cycle + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + + + 19 + 80 + 200 + 16 + + + + + 0 + 0 + 0 + + + + + 115 + 223 + 255 + + + + get Proposal and ESAF info + + + update + + + apsbss + + + update $(P) + + + + + + 259 + 80 + 100 + 16 + + + + + 0 + 0 + 0 + + + + + 115 + 223 + 255 + + + + clear PVs + + + clear + + + apsbss + + + clear $(P) + + + + + + 10 + 101 + 382 + 122 + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Proposal + + + ESimpleLabel::WidthAndHeight + + + + 7 + 5 + 120 + 16 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + caGraphics::Rectangle + + + + 0 + 0 + 380 + 120 + + + + 2 + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Solid + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + mail in + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 49 + 50 + 12 + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + title + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 66 + 50 + 12 + + + + + + + 67 + 66 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:title + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + users + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 83 + 50 + 12 + + + + + + + 67 + 83 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:users + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + + 67 + 100 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:userBadges + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + badges + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 100 + 50 + 12 + + + + + + + 65 + 50 + 80 + 12 + + + + $(P)proposal:mailInFlag + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + Column + + + caChoice::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + proprietary + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 150 + 50 + 100 + 12 + + + + + + + 255 + 51 + 80 + 12 + + + + $(P)proposal:proprietaryFlag + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + Column + + + caChoice::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ID + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 29 + 30 + 30 + 14 + + + + + + + 64 + 30 + 100 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:id + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + submitted + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 182 + 31 + 80 + 12 + + + + + + + 267 + 31 + 100 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)proposal:submittedDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + + + 10 + 226 + 382 + 132 + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ESAF + + + ESimpleLabel::WidthAndHeight + + + + 7 + 5 + 120 + 16 + + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + + + caGraphics::Rectangle + + + + 0 + 0 + 380 + 130 + + + + 2 + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + Solid + + + + + + 65 + 32 + 100 + 14 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:id + + + + 0 + 0 + 0 + + + + + 153 + 255 + 255 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + ID + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 30 + 32 + 30 + 14 + + + + + + + 237 + 32 + 100 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:status + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + status + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 170 + 32 + 61 + 12 + + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + dates + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 51 + 50 + 12 + + + + + + + 65 + 51 + 101 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:startDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + to + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 171 + 51 + 20 + 12 + + + + + + + 196 + 51 + 101 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:endDate + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + caLineEdit::Static + + + decimal + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + title + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 68 + 50 + 12 + + + + + + + 67 + 68 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:title + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + users + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 85 + 50 + 12 + + + + + + + 67 + 85 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:users + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + + 67 + 102 + 303 + 12 + + + + caLineEdit::WidthAndHeight + + + $(P)esaf:userBadges + + + + 0 + 0 + 0 + + + + + 235 + 241 + 181 + + + + caLineEdit::Channel + + + caLineEdit::Channel + + + caLineEdit::Channel + + + 0.0 + + + 1.0 + + + Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter + + + string + + + caLineEdit::Static + + + + + QFrame::NoFrame + + + + 0 + 0 + 0 + + + + + 0 + 0 + 0 + + + + badges + + + ESimpleLabel::WidthAndHeight + + + Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter + + + + 10 + 102 + 50 + 12 + + + + + caLabel_0 + caLabel_1 + caLabel_2 + caFrame_0 + caLabel_3 + caRectangle_0 + caLabel_4 + caLabel_5 + caLabel_6 + caLabel_7 + caLabel_8 + caLabel_9 + caLabel_10 + caFrame_1 + caLabel_11 + caRectangle_1 + caLabel_12 + caLabel_13 + caLabel_14 + caLabel_15 + caLabel_16 + caLabel_17 + caLabel_18 + caFrame_2 + caTextEntry_0 + caTextEntry_1 + caShellCommand_0 + caShellCommand_1 + caLineEdit_0 + caLineEdit_1 + caLineEdit_2 + caChoice_0 + caChoice_1 + caTextEntry_2 + caTextEntry_3 + caTextEntry_4 + caTextEntry_5 + caTextEntry_6 + caTextEntry_7 + caLineEdit_3 + caLineEdit_4 + caLineEdit_5 + + + \ No newline at end of file diff --git a/apstools/beamtime/apsbss_ioc.sh b/apstools/beamtime/apsbss_ioc.sh new file mode 100755 index 000000000..96403e467 --- /dev/null +++ b/apstools/beamtime/apsbss_ioc.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +# manage the IOC for apsbss + +#-------------------- +# change the program defaults here +DEFAULT_SESSION_NAME=apsbss +DEFAULT_IOC_PREFIX=ioc:bss: +#-------------------- + +SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}} +SELECTION=${1:-usage} +SESSION_NAME=${2:-"${DEFAULT_SESSION_NAME}"} +IOC_PREFIX=${3:-"${DEFAULT_IOC_PREFIX}"} + +IOC_BINARY=softIoc +EPICS_DATABASE=apsbss.db +START_IOC_COMMAND="${IOC_BINARY} -m P=${IOC_PREFIX} -d ${EPICS_DATABASE}" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +if [ -z "$IOC_STARTUP_DIR" ] ; then + # If no startup dir is specified, use the directory with this script + IOC_STARTUP_DIR=$(dirname "${SHELL_SCRIPT_NAME}") +fi + +# echo "SESSION_NAME = ${SESSION_NAME}" +# echo "IOC_PREFIX = ${IOC_PREFIX}" +# echo "START_IOC_COMMAND = ${START_IOC_COMMAND}" +# echo "SHELL_SCRIPT_NAME = ${SHELL_SCRIPT_NAME}" +# echo "IOC_STARTUP_DIR = ${IOC_STARTUP_DIR}" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function checkpid() { + MY_UID=$(id -u) + # # The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh + IOC_PID=$(pgrep "${IOC_BINARY}"\$ -u "${MY_UID}") + # #!echo "IOC_PID=${IOC_PID}" + + if [ "${IOC_PID}" != "" ] ; then + # Assume the IOC is down until proven otherwise + IOC_DOWN=1 + SCREEN_PID="" + + # At least one instance of the IOC binary is running; + # Find the binary that is associated with this script/IOC + for pid in ${IOC_PID}; do + # compare directories + BIN_CWD=$(readlink "/proc/${pid}/cwd") + IOC_CWD=$(readlink -f "${IOC_STARTUP_DIR}") + + if [ "$BIN_CWD" = "$IOC_CWD" ] ; then + # The IOC is running; + # the process with PID=$pid is the + # IOC that was run from $IOC_STARTUP_DIR + P_PID=$(ps -p "${pid}" -o ppid=) + # strip leading (and trailing) whitespace + arr=($P_PID) + P_PID=${arr[0]} + SCREEN_SESSION="${P_PID}.${SESSION_NAME}" + SCREEN_MATCH=$(screen -ls "${SCREEN_SESSION}" | grep "${SESSION_NAME}") + if [ "${SCREEN_MATCH}" != "" ] ; then + # IOC is running in screen + IOC_DOWN=0 + IOC_PID=${pid} + SCREEN_PID=${P_PID} + break + fi + fi + done + else + # IOC is not running + IOC_DOWN=1 + fi + + return ${IOC_DOWN} +} + +function console () { + if checkpid; then + echo "Connecting to ${SCREEN_SESSION}'s screen session" + # The -r flag will only connect if no one is attached to the session + #!screen -r "${SESSION_NAME}" + # The -x flag will connect even if someone is attached to the session + screen -x "${SCREEN_SESSION}" + else + echo "${SCREEN_NAME} is not running" + fi +} + +function exit_if_running() { + # ensure that multiple, simultaneous IOCs are not started by this user ID + MY_UID=$(id -u) + IOC_PID=$(pgrep "${SESSION_NAME}"\$ -u "${MY_UID}") + + if [ "" != "${IOC_PID}" ] ; then + echo "${SESSION_NAME} IOC is already running (PID=${IOC_PID}), won't start a new one" + exit 1 + fi +} + +function restart() { + stop + start +} + +function run_ioc() { + exit_if_running + ${START_IOC_COMMAND} +} + +function screenpid() { + if [ -z "${SCREEN_PID}" ] ; then + echo + else + echo " in a screen session (pid=${SCREEN_PID})" + fi +} + +function start() { + if checkpid; then + echo -n "${SCREEN_SESSION} is already running (pid=${IOC_PID})" + screenpid + else + echo "Starting ${SESSION_NAME} with IOC prefix ${IOC_PREFIX}" + cd "${IOC_STARTUP_DIR}" + # Run SESSION_NAME inside a screen session + CMD="screen -dm -S ${SESSION_NAME} -h 5000 ${START_IOC_COMMAND}" + $CMD + fi +} + +function status() { + if checkpid; then + echo -n "${SCREEN_SESSION} is running (pid=${IOC_PID})" + screenpid + else + echo "${SESSION_NAME} is not running" + fi +} + +function stop() { + if checkpid; then + echo "Stopping ${SCREEN_SESSION} (pid=${IOC_PID})" + kill "${IOC_PID}" + else + echo "${SESSION_NAME} is not running" + fi +} + +function usage() { + echo "Usage: $(basename "${SHELL_SCRIPT_NAME}") {start|stop|restart|status|console|run} [NAME [PREFIX]]" + echo "" + echo " COMMANDS" + echo " console attach to IOC console if IOC is running in screen" + echo " restart restart IOC" + echo " run run IOC in console (not screen)" + echo " start start IOC" + echo " status report if IOC is running" + echo " stop stop IOC" + echo "" + echo " OPTIONAL TERMS" + echo " NAME name of IOC session (default: ${DEFAULT_SESSION_NAME})" + echo " PREFIX IOC prefix (default: ${DEFAULT_IOC_PREFIX})" +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +case ${SELECTION} in + start) start ;; + stop | kill) stop ;; + restart) restart ;; + status) status ;; + console) console ;; + run) run_ioc ;; + *) usage ;; +esac diff --git a/apstools/beamtime/apsbss_makedb.py b/apstools/beamtime/apsbss_makedb.py new file mode 100755 index 000000000..d470b6aa8 --- /dev/null +++ b/apstools/beamtime/apsbss_makedb.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +""" +Create the EPICS database + +This is just a service program to make the ``.db`` file. + +.. autosummary:: + + ~main +""" + +#name RTYP length + +raw_data = """ +esaf:cycle stringout +esaf:description waveform 4096 +esaf:endDate stringout +esaf:id stringout +esaf:status stringout +esaf:sector stringout +esaf:startDate stringout +esaf:title waveform 1024 +esaf:userBadges waveform 1024 +esaf:users waveform 1024 +esaf:user1:badgeNumber stringout +esaf:user1:email stringout +esaf:user1:firstName stringout +esaf:user1:lastName stringout +esaf:user2:badgeNumber stringout +esaf:user2:email stringout +esaf:user2:firstName stringout +esaf:user2:lastName stringout +esaf:user3:badgeNumber stringout +esaf:user3:email stringout +esaf:user3:firstName stringout +esaf:user3:lastName stringout +esaf:user4:badgeNumber stringout +esaf:user4:email stringout +esaf:user4:firstName stringout +esaf:user4:lastName stringout +esaf:user5:badgeNumber stringout +esaf:user5:email stringout +esaf:user5:firstName stringout +esaf:user5:lastName stringout +esaf:user6:badgeNumber stringout +esaf:user6:email stringout +esaf:user6:firstName stringout +esaf:user6:lastName stringout +esaf:user7:badgeNumber stringout +esaf:user7:email stringout +esaf:user7:firstName stringout +esaf:user7:lastName stringout +esaf:user8:badgeNumber stringout +esaf:user8:email stringout +esaf:user8:firstName stringout +esaf:user8:lastName stringout +esaf:user9:badgeNumber stringout +esaf:user9:email stringout +esaf:user9:firstName stringout +esaf:user9:lastName stringout +proposal:beamline stringout +proposal:mailInFlag bo +proposal:id stringout +proposal:proprietaryFlag bo +proposal:submittedDate stringout +proposal:title waveform 1024 +proposal:userBadges waveform 1024 +proposal:users waveform 1024 +proposal:user1:badgeNumber stringout +proposal:user1:email stringout +proposal:user1:firstName stringout +proposal:user1:institution waveform 1024 +proposal:user1:instId stringout +proposal:user1:lastName stringout +proposal:user1:piFlag bo +proposal:user1:userId stringout +proposal:user2:badgeNumber stringout +proposal:user2:email stringout +proposal:user2:firstName stringout +proposal:user2:institution waveform 1024 +proposal:user2:instId stringout +proposal:user2:lastName stringout +proposal:user2:piFlag bo +proposal:user2:userId stringout +proposal:user3:badgeNumber stringout +proposal:user3:email stringout +proposal:user3:firstName stringout +proposal:user3:institution waveform 1024 +proposal:user3:instId stringout +proposal:user3:lastName stringout +proposal:user3:piFlag bo +proposal:user3:userId stringout +proposal:user4:badgeNumber stringout +proposal:user4:email stringout +proposal:user4:firstName stringout +proposal:user4:institution waveform 1024 +proposal:user4:instId stringout +proposal:user4:lastName stringout +proposal:user4:piFlag bo +proposal:user4:userId stringout +proposal:user5:badgeNumber stringout +proposal:user5:email stringout +proposal:user5:firstName stringout +proposal:user5:institution waveform 1024 +proposal:user5:instId stringout +proposal:user5:lastName stringout +proposal:user5:piFlag bo +proposal:user5:userId stringout +proposal:user6:badgeNumber stringout +proposal:user6:email stringout +proposal:user6:firstName stringout +proposal:user6:institution waveform 1024 +proposal:user6:instId stringout +proposal:user6:lastName stringout +proposal:user6:piFlag bo +proposal:user6:userId stringout +proposal:user7:badgeNumber stringout +proposal:user7:email stringout +proposal:user7:firstName stringout +proposal:user7:institution waveform 1024 +proposal:user7:instId stringout +proposal:user7:lastName stringout +proposal:user7:piFlag bo +proposal:user7:userId stringout +proposal:user8:badgeNumber stringout +proposal:user8:email stringout +proposal:user8:firstName stringout +proposal:user8:institution waveform 1024 +proposal:user8:instId stringout +proposal:user8:lastName stringout +proposal:user8:piFlag bo +proposal:user8:userId stringout +proposal:user9:badgeNumber stringout +proposal:user9:email stringout +proposal:user9:firstName stringout +proposal:user9:institution waveform 1024 +proposal:user9:instId stringout +proposal:user9:lastName stringout +proposal:user9:piFlag bo +proposal:user9:userId stringout +""".strip().splitlines() + + +def main(): + """Make an EPICS ``.db`` file from the PVs listed above.""" + header = """\ + # + # file: apsbss.db + # EPICS database for information from APS ESAF & Proposal databases + # + # BSS: Beamtime Scheduling System + # + # note: this file autogenerated by make_database.py + # ./apsbss_makedb.py | tee apsbss.db + # + # Start the EPICS IOC with a command (from EPICS base): + # softIoc -m P=ioc:bss: -d apsbss.db + """ + for line in header.splitlines(): + print(line.strip()) + + for row in raw_data: + parts = row.split() + pvname, rtyp = parts[:2] + head = f'record({rtyp}, "$(P){pvname}")' + fields = [] + if rtyp == "waveform" and len(parts) == 3: + length = parts[-1] + fields.append(f'field(FTVL, "CHAR")') + fields.append(f'field(NELM, {length})') + elif rtyp == "bo": + fields.append(f'field(ZNAM, "OFF")') + fields.append(f'field(ONAM, "ON")') + record = head + if len(fields) > 0: + field_specs = "\n".join([f" {field}" for field in fields]) + record += " {\n" + f"{field_specs}" + "\n}" + print("\n" + record) + + +if __name__ == "__main__": + main() diff --git a/apstools/beamtime/apsbss_ophyd.py b/apstools/beamtime/apsbss_ophyd.py new file mode 100644 index 000000000..60272113f --- /dev/null +++ b/apstools/beamtime/apsbss_ophyd.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python + +""" +ophyd support for apsbss + +EXAMPLE:: + + apsbss = EpicsBssDevice("ioc:bss:", name="apsbss") + +.. autosummary:: + + ~EpicsBssDevice + ~EpicsEsafDevice + ~EpicsEsafExperimenterDevice + ~EpicsProposalDevice + ~EpicsProposalExperimenterDevice + +""" + +__all__ = ["EpicsBssDevice",] + +from ophyd import Component, Device, EpicsSignal + + +class EpicsEsafExperimenterDevice(Device): + """ + Ophyd device for experimenter info from APS ESAF. + + .. autosummary:: + + ~clear + """ + badge_number = Component(EpicsSignal, "badgeNumber", string=True) + email = Component(EpicsSignal, "email", string=True) + first_name = Component(EpicsSignal, "firstName", string=True) + last_name = Component(EpicsSignal, "lastName", string=True) + + def clear(self): + """Clear the fields for this user.""" + self.badge_number.put("") + self.email.put("") + self.first_name.put("") + self.last_name.put("") + + +class EpicsEsafDevice(Device): + """ + Ophyd device for info from APS ESAF. + + .. autosummary:: + + ~clear + ~clear_users + """ + + aps_cycle = Component(EpicsSignal, "cycle", string=True) + description = Component(EpicsSignal, "description", string=True) + end_date = Component(EpicsSignal, "endDate", string=True) + esaf_id = Component(EpicsSignal, "id", string=True) + esaf_status = Component(EpicsSignal, "status", string=True) + sector = Component(EpicsSignal, "sector", string=True) + start_date = Component(EpicsSignal, "startDate", string=True) + title = Component(EpicsSignal, "title", string=True) + user_last_names = Component(EpicsSignal, "users", string=True) + user_badges = Component(EpicsSignal, "userBadges", string=True) + + _max_users = 9 # 9 users at most? + user1 = Component(EpicsEsafExperimenterDevice, "user1:") + user2 = Component(EpicsEsafExperimenterDevice, "user2:") + user3 = Component(EpicsEsafExperimenterDevice, "user3:") + user4 = Component(EpicsEsafExperimenterDevice, "user4:") + user5 = Component(EpicsEsafExperimenterDevice, "user5:") + user6 = Component(EpicsEsafExperimenterDevice, "user6:") + user7 = Component(EpicsEsafExperimenterDevice, "user7:") + user8 = Component(EpicsEsafExperimenterDevice, "user8:") + user9 = Component(EpicsEsafExperimenterDevice, "user9:") + + def clear(self): + """ + Clear the most of the ESAF info. + + Do not clear these items: + + * ``aps_cycle`` + * ``esaf_id`` + * ``sector`` + """ + # self.aps_cycle.put("") # user controls this + self.description.put("") + self.end_date.put("") + # self.esaf_id.put("") # user controls this + self.esaf_status.put("") + # self.sector.put("") + self.start_date.put("") + self.title.put("") + self.user_last_names.put("") + self.user_badges.put("") + + self.clear_users() + + def clear_users(self): + """Clear the info for all users.""" + self.user1.clear() + self.user2.clear() + self.user3.clear() + self.user4.clear() + self.user5.clear() + self.user6.clear() + self.user7.clear() + self.user8.clear() + self.user9.clear() + + +class EpicsProposalExperimenterDevice(Device): + """ + Ophyd device for experimenter info from APS Proposal. + + .. autosummary:: + + ~clear + """ + badge_number = Component(EpicsSignal, "badgeNumber", string=True) + email = Component(EpicsSignal, "email", string=True) + first_name = Component(EpicsSignal, "firstName", string=True) + institution = Component(EpicsSignal, "institution", string=True) + institution_id = Component(EpicsSignal, "instId", string=True) + last_name = Component(EpicsSignal, "lastName", string=True) + pi_flag = Component(EpicsSignal, "piFlag", string=True) + user_id = Component(EpicsSignal, "userId", string=True) + + def clear(self): + """Clear the info for this user.""" + self.badge_number.put("") + self.email.put("") + self.first_name.put("") + self.last_name.put("") + self.user_id.put("") + self.institution_id.put("") + self.institution.put("") + self.pi_flag.put(0) + + +class EpicsProposalDevice(Device): + """ + Ophyd device for info from APS Proposal. + + .. autosummary:: + + ~clear + ~clear_users + """ + + beamline_name = Component(EpicsSignal, "beamline", string=True) + + mail_in_flag = Component(EpicsSignal, "mailInFlag", string=True) + proposal_id = Component(EpicsSignal, "id", string=True) + proprietary_flag = Component(EpicsSignal, "proprietaryFlag", string=True) + submitted_date = Component(EpicsSignal, "submittedDate", string=True) + title = Component(EpicsSignal, "title", string=True) + user_last_names = Component(EpicsSignal, "users", string=True) + user_badges = Component(EpicsSignal, "userBadges", string=True) + + _max_users = 9 # 9 users at most? + user1 = Component(EpicsProposalExperimenterDevice, "user1:") + user2 = Component(EpicsProposalExperimenterDevice, "user2:") + user3 = Component(EpicsProposalExperimenterDevice, "user3:") + user4 = Component(EpicsProposalExperimenterDevice, "user4:") + user5 = Component(EpicsProposalExperimenterDevice, "user5:") + user6 = Component(EpicsProposalExperimenterDevice, "user6:") + user7 = Component(EpicsProposalExperimenterDevice, "user7:") + user8 = Component(EpicsProposalExperimenterDevice, "user8:") + user9 = Component(EpicsProposalExperimenterDevice, "user9:") + + def clear(self): + """ + Clear the most of the proposal info. + + Do not clear these items: + + * ``beamline_name`` + * ``proposal_id`` + """ + # self.beamline_name.put("") # user controls this + self.mail_in_flag.put(0) + # self.proposal_id.put(-1) # user controls this + self.proprietary_flag.put(0) + self.submitted_date.put("") + self.title.put("") + self.user_last_names.put("") + self.user_badges.put("") + + self.clear_users() + + def clear_users(self): + """Clear the info for all users.""" + self.user1.clear() + self.user2.clear() + self.user3.clear() + self.user4.clear() + self.user5.clear() + self.user6.clear() + self.user7.clear() + self.user8.clear() + self.user9.clear() + + +class EpicsBssDevice(Device): + """ + Ophyd device for info from APS Proposal and ESAF databases. + + .. autosummary:: + + ~clear + """ + + esaf = Component(EpicsEsafDevice, "esaf:") + proposal = Component(EpicsProposalDevice, "proposal:") + + def clear(self): + """Clear the proposal and ESAF info.""" + self.esaf.clear() + self.proposal.clear() diff --git a/apstools/devices.py b/apstools/devices.py index 71efdfbd5..3f3751265 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -1856,6 +1856,7 @@ def generate_datum(self, key, timestamp, datum_kwargs): datum_kwargs["HDF5_file_name"] = hdf5_file_name logger.debug("make_filename: %s", hdf5_file_name) + logger.debug("write_path: %s", write_path) return super().generate_datum(key, timestamp, datum_kwargs) def get_frames_per_point(self): diff --git a/apstools/filewriters.py b/apstools/filewriters.py index 95eaf8509..33a27bd7c 100644 --- a/apstools/filewriters.py +++ b/apstools/filewriters.py @@ -46,6 +46,7 @@ SPEC_TIME_FORMAT = "%a %b %d %H:%M:%S %Y" SCAN_ID_RESET_VALUE = 0 + def _rebuild_scan_command(doc): """ reconstruct the scan command for SPEC data file #S line @@ -657,7 +658,7 @@ def myPlan(): #C Mon Jan 28 12:48:14 2019. exit_status = success """ - # global specwriter # such as: specwriter = SpecWriterCallback() + global specwriter # such as: specwriter = SpecWriterCallback() writer = writer or specwriter if doc is None: if writer.scanning: diff --git a/apstools/plans.py b/apstools/plans.py index 96a073c7f..5eedb9045 100644 --- a/apstools/plans.py +++ b/apstools/plans.py @@ -87,7 +87,7 @@ def addDeviceDataAsStream(devices, label): yield from bps.save() -def execute_command_list(filename, commands, md={}): +def execute_command_list(filename, commands, md=None): """ plan: execute the command list @@ -206,7 +206,7 @@ def get_command_list(filename): def lineup( counter, axis, minus, plus, npts, time_s=0.1, peak_factor=4, width_factor=0.8, - _md={}): + md=None): """ lineup and center a given axis, relative to current position @@ -312,16 +312,16 @@ def peak_analysis(): bec.peaks.aligned = aligned bec.peaks.ATTRS = ('com', 'cen', 'max', 'min', 'fwhm') - md = dict(_md) - md["purpose"] = "alignment" - yield from bp.rel_scan([counter], axis, minus, plus, npts, md=md) + _md = dict(purpose="alignment") + _md.update(md or {}) + yield from bp.rel_scan([counter], axis, minus, plus, npts, md=_md) yield from peak_analysis() if bec.peaks.aligned: # again, tweak axis to maximize - md["purpose"] = "alignment - fine" + _md["purpose"] = "alignment - fine" fwhm = bec.peaks["fwhm"][counter.name] - yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=md) + yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=_md) yield from peak_analysis() if scaler is not None: @@ -594,7 +594,7 @@ def register_command_handler(handler=None): _COMMAND_HANDLER_ = handler or execute_command_list -def run_command_file(filename, md={}): +def run_command_file(filename, md=None): """ plan: execute a list of commands from a text or Excel file @@ -614,8 +614,10 @@ def run_command_file(filename, md={}): *new in apstools release 1.1.7* """ + _md = dict(command_file=filename) + _md.update(md or {}) commands = get_command_list(filename) - yield from _COMMAND_HANDLER_(filename, commands) + yield from _COMMAND_HANDLER_(filename, commands, md=_md) def snapshot(obj_list, stream="primary", md=None): @@ -738,7 +740,7 @@ def sscan_1D( running_stream="primary", final_array_stream=None, device_settings_stream="settings", - md={}): + md=None): """ simple 1-D scan using EPICS synApps sscan record @@ -836,9 +838,10 @@ def phase_cb(value, timestamp, **kwargs): # watch for new data to be read out sscan.scan_phase.subscribe(phase_cb) - md["plan_name"] = "sscan_1D" + _md = dict(plan_name="sscan_1D") + _md.update(md or {}) - yield from bps.open_run(md) # start data collection + yield from bps.open_run(_md) # start data collection yield from bps.mv(sscan.execute_scan, 1) # start sscan started = True diff --git a/apstools/utils.py b/apstools/utils.py index 3493b7892..305602911 100644 --- a/apstools/utils.py +++ b/apstools/utils.py @@ -30,6 +30,7 @@ ~safe_ophyd_name ~show_ophyd_symbols ~split_quoted_line + ~summarize_runs ~text_encode ~to_unicode_or_bust ~trim_string_for_EPICS @@ -49,8 +50,9 @@ from bluesky.callbacks.best_effort import BestEffortCallback from bluesky import plan_stubs as bps -from collections import OrderedDict +from collections import defaultdict, OrderedDict import databroker +import databroker.queries import datetime from email.mime.text import MIMEText from event_model import NumpyEncoder @@ -237,7 +239,7 @@ def itemizer(fmt, items): def listruns( - num=20, keys=[], printing=True, + num=20, keys=None, printing=True, show_command=True, db=None, exit_status=None, **db_search_terms): @@ -308,6 +310,7 @@ def listruns( *new in apstools release 1.1.10* """ db = db or ipython_shell_namespace()["db"] + keys = keys or [] if show_command: labels = "scan_id command".split() + keys @@ -726,6 +729,65 @@ def split_quoted_line(line): return parts +def summarize_runs(since=None, db=None): + """ + Report bluesky run metrics from the databroker. + + * How many different plans? + * How many runs? + * How many times each run was used? + * How frequently? (TODO:) + + PARAMETERS + + since (str) : + Report all runs since this ISO8601 date & time (default: ``1995``) + db (object) : + Instance of ``databroker.Broker()`` + (default: ``db`` from the IPython shell) + """ + db = db or ipython_shell_namespace()["db"] + since = since or "1995" # no APS X-ray experiment data before 1995! + cat = db.v2.search(databroker.queries.TimeRange(since=since)) + plans = defaultdict(list) + t0 = time.time() + for n, uid in enumerate(cat): + t1 = time.time() + run = cat[uid] # this step is very slow (0.01 - 0.5 seconds each!) + t2 = time.time() + plan_name = run.metadata["start"].get("plan_name", "unknown") + dt = datetime.datetime.fromtimestamp(run.metadata["start"]["time"]).isoformat() + scan_id = run.metadata["start"].get("scan_id", "unknown") + plans[plan_name].append( + dict( + plan_name=plan_name, + dt=dt, + time_start=dt, + uid=uid, + scan_id=scan_id, + ) + ) + logger.debug( + "%s %s dt1=%4.01fus dt2=%5.01fms %s", + scan_id, + dt, + (t1-t0)*1e6, + (t2-t1)*1e3, + plan_name, + ) + t0 = time.time() + + def sorter(plan_name): + return len(plans[plan_name]) + + table = pyRestTable.Table() + table.labels = "plan quantity".split() + for k in sorted(plans.keys(), key=sorter, reverse=True): + table.addRow((k, sorter(k))) + table.addRow(("TOTAL", n+1)) + print(table) + + def text_encode(source): """Encode ``source`` using the default codepoint.""" return source.encode(errors='ignore') diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 7a0368df9..d4e579c52 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -26,6 +26,7 @@ build: script: python -m pip install --no-deps --ignore-installed . noarch: python entry_points: + - apsbss = apstools.beamtime.apsbss:main - apstools_plan_catalog = apstools.examples:main - bluesky_snapshot = apstools.snapshot:snapshot_cli - bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui @@ -35,9 +36,11 @@ requirements: - python - pip run: - - python + - python>=3.6.0 + - aps-dm-api - bluesky>=1.6.2 - databroker>=1.0.6 + - h5py - ophyd>=1.5.1 - pandas - pyEpics>=3.4.2 diff --git a/create_release_notes.py b/create_release_notes.py index f4dda3445..b368171fa 100755 --- a/create_release_notes.py +++ b/create_release_notes.py @@ -37,13 +37,16 @@ def findGitConfigFile(): Needs testing for when things are wrong. """ path = os.getcwd() - for i in range(99): + while len(path.split(os.path.sep)): config_file = os.path.join(path, ".git", "config") if os.path.exists(config_file): return config_file # found it! # next, look in the parent directory - path = os.path.abspath(os.path.join(path, "..")) + parent = os.path.dirname(path) + if parent == path: + break + path = parent msg = "Could not find .git/config file in any parent directory." logger.error(msg) diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst new file mode 100644 index 000000000..a1e55ed13 --- /dev/null +++ b/docs/source/applications/apsbss.rst @@ -0,0 +1,450 @@ + + +.. index:: apsbss + +.. _apsbss_application: + +apsbss +------ + +Provide information from APS Proposal and ESAF (experiment safety approval +form) databases as PVs at each beam line so that this information +(metadata) may be added to new data files. The ``aps-dm-api`` +(``dm`` for short) package [#]_ +is used to access the APS databases as read-only. + +*No information is written back to the APS +databases from this software.* + +.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api + +.. sidebar:: PVs for experiment metadata + + This information retreived from the APS databases is stored in PVs + on the beam line subnet. These PVs are available to *any* EPICS + client as metadata (SPEC, area detector, Bluesky, GUI screens, logging, other). + This design allows the local instrument team to override + any values read from the APS databases, if that is needed. + +Given: + +* a beam line name (such as ``9-ID-B,C``) +* APS run cycle name (such as ``2019-2``) to locate a specific proposal ID +* proposal ID number (such as ``66083``) +* ESAF ID number (such as ``226319``) + +The typical information obtained includes: + +* ESAF & proposal titles +* user names +* user institutional affiliations +* user emails +* applicable dates, reported in ISO8601 time format +* is proposal propietary? +* is experiment mail-in? + +These PVs are loaded on demand by the local instrument team at the beam line. +See the :ref:`apsbss_startup` section for details about +managing the EPICS PVs. + + +Overview +++++++++ + +We'll demonstrate ``apsbss`` with information for APS beam +line 9-ID, using PV prefix ``9id:bss:``. + +#. Create the PVs in an EPICS IOC +#. Initialize PVs with beam line name and APS run cycle number +#. Set PVs with the Proposal and ESAF ID numbers +#. Retrieve (& update PVs) information from APS databases + +**Enter beam line and APS run (cycle) info** + +.. figure:: ../resources/ui_initialized.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix + (``9id:bss:``), APS run cycle ``2019-2`` and beam line ``9-ID-B,C``. + + * beam line name PV: ``9id:bss:proposal:beamline`` + * APS run cycle PV: ``9id:bss:esaf:cycle`` + + +**Enter Proposal and ESAF ID numbers** + +Note we had to use the APS run cycle of `2019-2` +to match what is in the proposal's information. + +.. figure:: ../resources/ui_id_entered.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal + and ESAF ID numbers added. + + * proposal ID number PV: ``9id:bss:proposal:id`` + * ESAF ID number PV: ``9id:bss:esaf:id`` + +**Update PVs from APS databases** + +In the GUI, press the button labeled ``get Proposal and ESAF info``. +This button executes the command line: ``apsbss update 9id:bss:`` + +Here's a view of the GUI after running the update. The +information shown in the GUI is only part of the PVs, +presented in a compact format. A full report of the +information received, including PV names, is available for +:download:`download <../resources/apsbss_report.txt>`. + +.. figure:: ../resources/ui_updated.png + :width: 95% + + Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal + and ESAF information. + +To clear the PVs, in the GUI, press the button labeled ``clear PVs``. +This button executes the command line: ``apsbss clear 9id:bss:`` + + +Initialize PVs for beam line and APC run cycle +++++++++++++++++++++++++++++++++++++++++++++++ + +After creating the PVs in an IOC, the next step is to +initialize them with the beam line name and the APS +run cycle name. Both of these must match exactly +with values known in the data management (``dm``) system. + +For any of these commands, you must know the EPICS +PV prefix to be used. The examples above are for +beam line 9-ID. The PV prefix in these examples +is ``9id:bss:``. + + +What beam line name to use? +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To learn the beam line names accepted by the system, use this command +(showing names defined on 2020-07-10):: + + $ apsbss beamlines + 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B + 1-ID-B,C,E 9-BM-B,C 16-BM-B 23-ID-B + 2-BM-A,B 9-ID-B,C 16-BM-D 23-ID-D + 2-ID-D 10-BM-A,B 16-ID-B 24-ID-C + 2-ID-E 10-ID-B 16-ID-D 24-ID-E + 3-ID-B,C,D 11-BM-B 17-BM-B 26-ID-C + 4-ID-C 11-ID-B 17-ID-B 27-ID-B + 4-ID-D 11-ID-C 18-ID-D 29-ID-C,D + 5-BM-C 11-ID-D 19-BM-D 30-ID-B,C + 5-BM-D 12-BM-B 19-ID-D 31-ID-D + 5-ID-B,C,D 12-ID-B 20-BM-B 32-ID-B,C + 6-BM-A,B 12-ID-C,D 20-ID-B,C 33-BM-C + 6-ID-B,C 13-BM-C 21-ID-D 33-ID-D,E + 6-ID-D 13-BM-D 21-ID-E 34-ID-C + 7-BM-B 13-ID-C,D 21-ID-F 34-ID-E + 7-ID-B,C,D 13-ID-E 21-ID-G 35-ID-B,C,D,E + 8-BM-B 14-BM-C 22-BM-D + 8-ID-E 14-ID-B 22-ID-D + +For either station at 9-ID, use ``9-ID-B,C``. + + +What APS run cycle to use? +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To learn the APS run cycle names accepted by the system, use this command +(showing APS run cycle names defined on 2020-07-10):: + + $ apsbss cycles + 2008-3 2011-2 2014-1 2016-3 2019-2 + 2009-1 2011-3 2014-2 2017-1 2019-3 + 2009-2 2012-1 2014-3 2017-2 2020-1 + 2009-3 2012-2 2015-1 2017-3 2020-2 + 2010-1 2012-3 2015-2 2018-1 + 2010-2 2013-1 2015-3 2018-2 + 2010-3 2013-2 2016-1 2018-3 + 2011-1 2013-3 2016-2 2019-1 + +Pick the cycle of interest. Here, we pick ``2020-2``. + +To print the full report (including start and end of each cycle):: + + $ apsbss cycles --full + ====== =================== =================== + cycle start end + ====== =================== =================== + 2020-2 2020-06-09 07:00:00 2020-10-01 07:00:00 + 2020-1 2020-01-28 08:00:00 2020-06-09 07:00:00 + 2019-3 2019-09-24 07:00:00 2020-01-28 08:00:00 + 2019-2 2019-05-21 07:00:00 2019-09-24 07:00:00 + 2019-1 2019-01-22 08:00:00 2019-05-21 07:00:00 + 2018-3 2018-09-25 07:00:00 2019-01-22 08:00:00 + 2018-2 2018-05-22 07:00:00 2018-09-25 07:00:00 + 2018-1 2018-01-23 08:00:00 2018-05-22 07:00:00 + 2017-3 2017-09-26 07:00:00 2018-01-23 08:00:00 + 2017-2 2017-05-23 07:00:00 2017-09-26 07:00:00 + 2017-1 2017-01-24 08:00:00 2017-05-23 07:00:00 + 2016-3 2016-09-27 07:00:00 2017-01-24 08:00:00 + 2016-2 2016-05-24 07:00:00 2016-09-27 07:00:00 + 2016-1 2016-01-26 08:00:00 2016-05-24 07:00:00 + 2015-3 2015-09-29 07:00:00 2016-01-26 08:00:00 + 2015-2 2015-05-26 07:00:00 2015-09-29 07:00:00 + 2015-1 2015-01-27 08:00:00 2015-05-26 07:00:00 + 2014-3 2014-09-25 07:00:00 2015-01-27 08:00:00 + 2014-2 2014-05-20 07:00:00 2014-09-25 07:00:00 + 2014-1 2014-01-21 08:00:00 2014-05-20 07:00:00 + 2013-3 2013-09-24 07:00:00 2014-01-21 08:00:00 + 2013-2 2013-05-22 07:00:00 2013-09-24 07:00:00 + 2013-1 2013-01-22 08:00:00 2013-05-22 07:00:00 + 2012-3 2012-09-25 07:00:00 2013-01-22 08:00:00 + 2012-2 2012-05-23 07:00:00 2012-09-25 07:00:00 + 2012-1 2012-01-24 08:00:00 2012-05-23 07:00:00 + 2011-3 2011-09-27 07:00:00 2012-01-24 08:00:00 + 2011-2 2011-05-25 07:00:00 2011-09-27 07:00:00 + 2011-1 2011-01-25 08:00:00 2011-05-25 07:00:00 + 2010-3 2010-09-27 23:00:00 2011-01-25 08:00:00 + 2010-2 2010-05-26 07:00:00 2010-09-28 07:00:00 + 2010-1 2010-01-26 08:00:00 2010-05-26 07:00:00 + 2009-3 2009-09-29 07:00:00 2010-01-26 08:00:00 + 2009-2 2009-05-20 07:00:00 2009-09-29 07:00:00 + 2009-1 2009-01-21 08:00:00 2009-05-20 07:00:00 + 2008-3 2008-09-24 07:00:00 2009-01-21 08:00:00 + ====== =================== =================== + + +Write the beam line name and cycle to the PVs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To configure ``9id:bss:`` PVs for beam line +``9-ID-B,C`` and cycle ``2020-2``, +use this command:: + + $ apsbss setup 9id:bss: 9-ID-B,C 2020-2 + connected in 0.143s + setup EPICS 9id:bss: 9-ID-B,C cycle=2020-2 sector=9 + +Or you could enter them into the appropriate boxes on the GUI. + + +What Proposal and ESAF ID numbers to use? ++++++++++++++++++++++++++++++++++++++++++ + +Proposals are usually valid for two years. To learn what +proposals are valid for your beam line, use this command +with your own beam line's name. The report will provide +two tables, one for ESAFs for the current cycle and the +other for proposals +within the last two years (6 APS cycles):: + + $ apsbss current 9id:bss: 9-ID-B,C + Current Proposal(s) on 9-ID-B,C + + ===== ====== =================== ==================== ======================================== + id cycle date user(s) title + ===== ====== =================== ==================== ======================================== + 57504 2019-3 2017-10-27 15:31:46 Zhang,Levine,Long... Towards USAXS/SAXS/WAXS Characterizat... + 55236 2019-2 2017-07-07 12:32:39 Du,Vacek,Syed,Hon... Developing 3D cryo ptychography at th... + 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + 62490 2019-1 2018-10-25 11:10:49 Ilavsky,Frith,Sun Dissolution of nano-precipitates in m... + ===== ====== =================== ==================== ======================================== + + Current ESAF(s) on sector 9 + + ====== ======== ========== ========== ==================== ======================================== + id status start end user(s) title + ====== ======== ========== ========== ==================== ======================================== + 221805 Approved 2020-02-18 2020-12-25 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + 226572 Approved 2020-06-10 2020-09-28 Sterbinsky,Heald,... 9BM Beamline Commissioning 2020-2 + 226612 Approved 2020-06-10 2020-09-28 Chen,Deng,Yao,Jia... Bionanoprobe commissioning + ====== ======== ========== ========== ==================== ======================================== + +Note that some of the information in the tables above has been removed for brevity. + + +View Proposal Information ++++++++++++++++++++++++++ + +To view information about a specific proposal, you +must be able to provide the proposal's ID number and +the APS run cycle name. + +:: + + $ apsbss proposal 64629 2019-2 9-ID-B,C + duration: 36000 + endTime: '2019-06-25 17:00:00' + experimenters: + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Ilavsky + - badge: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: John + id: number_here + instId: 3927 + institution: Argonne National Laboratory + lastName: Okasinski + piFlag: Y + id: 64629 + mailInFlag: N + proprietaryFlag: N + startTime: '2019-06-25 07:00:00' + submittedDate: '2019-03-01 18:35:02' + title: 2019 National School on Neutron & X-ray Scattering Beamline Practicals - CMS + totalShiftsRequested: 12 + + +Get ESAF Information +++++++++++++++++++++ + +To view information about a specific ESAF, you +must be able to provide the ESAF ID number. + +:: + + $ apsbss esaf 226319 + description: We will commission beamline and USAXS instrument. We will perform experiments + with safe beamline standards and test samples (all located at beamline and used + for this purpose routinely) to evaluate performance of beamline and instrument. + We will perform hardware and software development as needed. + esafId: 226319 + esafStatus: Approved + esafTitle: Commission 9ID and USAXS + experimentEndDate: '2020-09-28 08:00:00' + experimentStartDate: '2020-05-26 08:00:00' + experimentUsers: + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Jan + lastName: Ilavsky + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Evan + lastName: Maxey + - badge: 'text_number_here' + badgeNumber: 'text_number_here' + email: uuuuuuuuuu@email.fqdn + firstName: Ivan + lastName: Kuzmenko + sector: 09 + + +Update EPICS PVs with Proposal and ESAF ++++++++++++++++++++++++++++++++++++++++ + +To update the PVs with Proposal and Information from the APS +database, first enter the proposal and ESAF ID numbers into +the GUI (or set the ``9id:bss:proposal:id``, and respectively). +Note that for this ESAF ID, we had to change the cycle to `2019-2`. + +Then, use this command to retrieve the information and update +the PVs:: + + $ apsbss update 9id:bss: + update EPICS 9id:bss: + connected in 0.105s + + +Clear the EPICS PVs ++++++++++++++++++++ + +To clear the information from the PVs, use this command:: + + $ apsbss clear 9id:bss: + clear EPICS 9id:bss: + connected in 0.104s + cleared in 0.011s + + +Report information in the EPICS PVs ++++++++++++++++++++++++++++++++++++ + +To view all the information in the PVs, use this command:: + + $ apsbss report 9id:bss: + clear EPICS 9id:bss: + +Since this content is rather large, it is available +for download: :download:`apsbss report <../resources/apsbss_report.txt>` + + +Example - ``apsbss`` command line ++++++++++++++++++++++++++++++++++ + +Before using the command-line interface, find out what +the *apsbss* application expects:: + + $ apsbss -h + usage: apsbss [-h] [-v] + {beamlines,current,cycles,esaf,proposal,clear,setup,update,report} + ... + + Retrieve specific records from the APS Proposal and ESAF databases. + + optional arguments: + -h, --help show this help message and exit + -v, --version print version number and exit + + subcommand: + {beamlines,current,cycles,esaf,proposal,clear,setup,update,report} + beamlines print list of beamlines + current print current ESAF(s) and proposal(s) + cycles print APS run cycle names + esaf print specific ESAF + proposal print specific proposal + clear EPICS PVs: clear + setup EPICS PVs: setup + update EPICS PVs: update from BSS + report EPICS PVs: report what is in the PVs + +See :ref:`beamtime_source_docs` for the source code documentation +of each of these subcommands. + +.. _apsbss_epics_gui_screens: + +Displays for MEDM & caQtDM +++++++++++++++++++++++++++ + +Display screen files are provided for viewing some of the EPICS PVs +using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``). + +* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` +* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` + +Start caQtDM with this command: ``caQtDM -macro "P=9id:bss:" apsbss.ui &`` + +Start MEDM with this command: ``medm -x -macro "P=9id:bss:" apsbss.ui &`` + +IOC Management +++++++++++++++ + +The EPICS PVs are provided by running an instance of ``apsbss.db`` +either in an existing EPICS IOC or using the ``softIoc`` application +from EPICS base. A shell script (``apsbss_ioc.sh``) is included +for loading Proposal and ESAF information from the +APS databases into the IOC. + +* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` + +See the section titled ":ref:`apsbss_startup`" +for the management of the EPICS IOC. + +Downloads ++++++++++ + +* EPICS database: :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` +* EPICS IOC shell script :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>` +* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>` +* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>` + +Source code documentation ++++++++++++++++++++++++++ + +See :ref:`beamtime_source_docs` for the source code documentation. diff --git a/docs/source/applications/apsbss_ioc.rst b/docs/source/applications/apsbss_ioc.rst new file mode 100644 index 000000000..a5efe41b6 --- /dev/null +++ b/docs/source/applications/apsbss_ioc.rst @@ -0,0 +1,165 @@ +.. _apsbss_startup: + +apsbss: IOC Startup and Management +================================== + +The :ref:`apsbss_application` software +provides information from the APS Proposal +and ESAF (experiment safety approval +form) databases as PVs to the local controls network. +The ``aps-dm-api`` (``dm`` for short) package [#]_ +is used to access the APS databases as read-only. +The information in the PVs can be used as metadata +for inclusion in data files produced + +*No information is written back to the APS +databases from this software.* + +.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api + + +Overview +-------- + +#. Create the PVs in an EPICS IOC +#. Initialize PVs with beam line name and APS run cycle number +#. Set PV with the Proposal ID number +#. Set PV with the ESAF ID number +#. Retrieve (& update PVs) information from APS databases + + +.. _apsbss_ioc_management: + +Start EPICS IOC +--------------- + +The EPICS Process Variables (PVs) that support this software +are provided by an EPICS PV server (IOC). The PVs are defined +by including the ``apsbss.db`` database file in the startup +of the IOC. The database can be add to an existing IOC +or run as a stand-alone IOC using the ``softIoc`` application +from EPICS base. + +To ensure that we create PVs with unique names, decide what +prefix to use with the EPICS database. Such as, for APS beam +line 9-ID, you might pick: ``9id:bss:`` (making sure to end +the prefix with the customary ``:``). + +Add EPICS database to existing IOC +++++++++++++++++++++++++++++++++++ + +To include ``apsbss.db`` in an existing IOC, copy the file +(see Downloads below) into the IOC's startup directory +(typically the directory with ``st.cmd``). Edit the ``st.cmd`` +file and a line like this just before the call to ``iocInit``:: + + dbLoadRecords("apsbss.db", "P=9id:bss:") + +Once the IOC is started, these PVs should be available to any +EPICS client on the local network. + +Run EPICS database in softIoc from EPICS Base ++++++++++++++++++++++++++++++++++++++++++++++ + +For this example, we pick a unique name for this process, +(``ioc9idbss``) based on the PV prefix (``9id:bss:``). + +.. sidebar:: TIP + + If you customize your copy of ``apsbss_ioc.sh`` + and pre-define the two lines:: + + # change the program defaults here + DEFAULT_SESSION_NAME=apsbss + DEFAULT_IOC_PREFIX=ioc:bss: + + then you do not need to supply these terms as + command-line arguments. The usage command + will show these the default names you provided. + +**Start** the IOC using the ``apsbss_ioc.sh`` tool +(as described below), with this command:: + + $ apsbss_ioc.sh start ioc9idbss 9id:bss: + Starting ioc9idbss with IOC prefix 9id:bss: + +**Stop** the IOC with this command:: + + $ apsbss_ioc.sh stop ioc9idbss 9id:bss: + ioc9idbss is not running + +The IOC **restart** command, will first stop the IOC (if +it is running), then start it. + +Report whether or not the IOC is running with this command:: + + $ apsbss_ioc.sh status ioc9idbss 9id:bss: + ioc9idbss is not running + +To interact with the **console** of an IOC running in +a ``screen`` session:: + + $ apsbss_ioc.sh console ioc9idbss 9id:bss: + +To end this interaction, type ``^A`` then ``D`` which will +leave the IOC running. Type ``exit`` to stop the IOC from +the console. + +For diagnostic (or other) purposes, you can also run the IOC +without using a screen session. This is the command:: + + $ apsbss_ioc.sh run ioc9idbss 9id:bss: + dbLoadDatabase("/home/beams1/JEMIAN/.conda/envs/bluesky_2020_5/epics/bin/linux-x86_64/../../dbd/softIoc.dbd") + softIoc_registerRecordDeviceDriver(pdbbase) + dbLoadRecords("apsbss.db", "P=9id:bss:") + iocInit() + Starting iocInit + ############################################################################ + ## EPICS R7.0.4 + ## Rev. 2020-05-29T13:39+0000 + ############################################################################ + cas warning: Configured TCP port was unavailable. + cas warning: Using dynamically assigned TCP port 39809, + cas warning: but now two or more servers share the same UDP port. + cas warning: Depending on your IP kernel this server may not be + cas warning: reachable with UDP unicast (a host's IP in EPICS_CA_ADDR_LIST) + iocRun: All initialization complete + epics> + +You should see the IOC shell prompt (``epics> ``). If you type ``exit`` +or otherwise close the session, the IOC will exit. + + +Shell Script to manage softIoc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To run a stand-alone IOC using the ``softIoc`` application +from EPICS base, use the supplied IOC management shell script +``apsbss_ioc.sh``:: + + $ apsbss_ioc.sh + Usage: apsbss_ioc.sh {start|stop|restart|status|console|run} [NAME [PREFIX]] + + COMMANDS + console attach to IOC console if IOC is running in screen + restart restart IOC + run run IOC in console (not screen) + start start IOC + status report if IOC is running + stop stop IOC + + OPTIONAL TERMS + NAME name of IOC session (default: apsbss) + PREFIX IOC prefix (default: ioc:bss:) + +* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>` +* :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>` + +.. note:: The shell script assumes that a working ``softIoc`` application + (from EPICS base) is in your executable ``$PATH``. You should confirm + this first before trying to start the IOC. + +.. note:: The ``softIoc`` application is run within a ``screen`` + session so that it remains running even if you close the + console session. Confirm that you have the ``screen`` application + first before trying to start the IOC. \ No newline at end of file diff --git a/docs/source/applications/index.rst b/docs/source/applications/index.rst index 64f2d05aa..affabe88f 100644 --- a/docs/source/applications/index.rst +++ b/docs/source/applications/index.rst @@ -8,14 +8,17 @@ Applications :maxdepth: 2 :glob: + apsbss + apsbss_ioc snapshot spec2ophyd -There are two command-line applications provided by apstools: +There are the applications provided by apstools: ===================================================== ================================= application purpose ===================================================== ================================= +:ref:`apsbss_application` Information from the APS Proposal and ESAF databases. :ref:`apstools_plan_catalog ` summary list of all scans in the databroker :ref:`bluesky_snapshot` Take a snapshot of a list of EPICS PVs and record it in the databroker. :ref:`spec2ophyd` read SPEC config file and convert to ophyd setup commands diff --git a/docs/source/resources/apsbss_report.txt b/docs/source/resources/apsbss_report.txt new file mode 100644 index 000000000..a39782612 --- /dev/null +++ b/docs/source/resources/apsbss_report.txt @@ -0,0 +1,130 @@ +============================= ================================== ============================== +name PV reference value +============================= ================================== ============================== +esaf.aps_cycle 9id:bss:esaf:cycle 2019-2 +esaf.description 9id:bss:esaf:description We will commission beamline an +esaf.end_date 9id:bss:esaf:endDate 2020-09-28 08:00:00 +esaf.esaf_id 9id:bss:esaf:id 226319 +esaf.esaf_status 9id:bss:esaf:status Approved +esaf.sector 9id:bss:esaf:sector 9 +esaf.start_date 9id:bss:esaf:startDate 2020-05-26 08:00:00 +esaf.title 9id:bss:esaf:title Commission 9ID and USAXS +esaf.user1.badge_number 9id:bss:esaf:user1:badgeNumber 86312 +esaf.user1.email 9id:bss:esaf:user1:email ilavsky@aps.anl.gov +esaf.user1.first_name 9id:bss:esaf:user1:firstName Jan +esaf.user1.last_name 9id:bss:esaf:user1:lastName Ilavsky +esaf.user2.badge_number 9id:bss:esaf:user2:badgeNumber 53748 +esaf.user2.email 9id:bss:esaf:user2:email emaxey@aps.anl.gov +esaf.user2.first_name 9id:bss:esaf:user2:firstName Evan +esaf.user2.last_name 9id:bss:esaf:user2:lastName Maxey +esaf.user3.badge_number 9id:bss:esaf:user3:badgeNumber 64065 +esaf.user3.email 9id:bss:esaf:user3:email kuzmenko@aps.anl.gov +esaf.user3.first_name 9id:bss:esaf:user3:firstName Ivan +esaf.user3.last_name 9id:bss:esaf:user3:lastName Kuzmenko +esaf.user4.badge_number 9id:bss:esaf:user4:badgeNumber +esaf.user4.email 9id:bss:esaf:user4:email +esaf.user4.first_name 9id:bss:esaf:user4:firstName +esaf.user4.last_name 9id:bss:esaf:user4:lastName +esaf.user5.badge_number 9id:bss:esaf:user5:badgeNumber +esaf.user5.email 9id:bss:esaf:user5:email +esaf.user5.first_name 9id:bss:esaf:user5:firstName +esaf.user5.last_name 9id:bss:esaf:user5:lastName +esaf.user6.badge_number 9id:bss:esaf:user6:badgeNumber +esaf.user6.email 9id:bss:esaf:user6:email +esaf.user6.first_name 9id:bss:esaf:user6:firstName +esaf.user6.last_name 9id:bss:esaf:user6:lastName +esaf.user7.badge_number 9id:bss:esaf:user7:badgeNumber +esaf.user7.email 9id:bss:esaf:user7:email +esaf.user7.first_name 9id:bss:esaf:user7:firstName +esaf.user7.last_name 9id:bss:esaf:user7:lastName +esaf.user8.badge_number 9id:bss:esaf:user8:badgeNumber +esaf.user8.email 9id:bss:esaf:user8:email +esaf.user8.first_name 9id:bss:esaf:user8:firstName +esaf.user8.last_name 9id:bss:esaf:user8:lastName +esaf.user9.badge_number 9id:bss:esaf:user9:badgeNumber +esaf.user9.email 9id:bss:esaf:user9:email +esaf.user9.first_name 9id:bss:esaf:user9:firstName +esaf.user9.last_name 9id:bss:esaf:user9:lastName +esaf.user_badges 9id:bss:esaf:userBadges 86312,53748,64065 +esaf.user_last_names 9id:bss:esaf:users Ilavsky,Maxey,Kuzmenko +proposal.beamline_name 9id:bss:proposal:beamline 9-ID-B,C +proposal.mail_in_flag 9id:bss:proposal:mailInFlag OFF +proposal.proposal_id 9id:bss:proposal:id 64629 +proposal.proprietary_flag 9id:bss:proposal:proprietaryFlag OFF +proposal.submitted_date 9id:bss:proposal:submittedDate 2019-03-01 18:35:02 +proposal.title 9id:bss:proposal:title 2019 National School on Neutro +proposal.user1.badge_number 9id:bss:proposal:user1:badgeNumber 86312 +proposal.user1.email 9id:bss:proposal:user1:email ilavsky@aps.anl.gov +proposal.user1.first_name 9id:bss:proposal:user1:firstName Jan +proposal.user1.institution 9id:bss:proposal:user1:institution Argonne National Laboratory +proposal.user1.institution_id 9id:bss:proposal:user1:instId 3927 +proposal.user1.last_name 9id:bss:proposal:user1:lastName Ilavsky +proposal.user1.pi_flag 9id:bss:proposal:user1:piFlag OFF +proposal.user1.user_id 9id:bss:proposal:user1:userId 424292 +proposal.user2.badge_number 9id:bss:proposal:user2:badgeNumber 85283 +proposal.user2.email 9id:bss:proposal:user2:email okasinski@aps.anl.gov +proposal.user2.first_name 9id:bss:proposal:user2:firstName John +proposal.user2.institution 9id:bss:proposal:user2:institution Argonne National Laboratory +proposal.user2.institution_id 9id:bss:proposal:user2:instId 3927 +proposal.user2.last_name 9id:bss:proposal:user2:lastName Okasinski +proposal.user2.pi_flag 9id:bss:proposal:user2:piFlag ON +proposal.user2.user_id 9id:bss:proposal:user2:userId 424308 +proposal.user3.badge_number 9id:bss:proposal:user3:badgeNumber +proposal.user3.email 9id:bss:proposal:user3:email +proposal.user3.first_name 9id:bss:proposal:user3:firstName +proposal.user3.institution 9id:bss:proposal:user3:institution +proposal.user3.institution_id 9id:bss:proposal:user3:instId +proposal.user3.last_name 9id:bss:proposal:user3:lastName +proposal.user3.pi_flag 9id:bss:proposal:user3:piFlag OFF +proposal.user3.user_id 9id:bss:proposal:user3:userId +proposal.user4.badge_number 9id:bss:proposal:user4:badgeNumber +proposal.user4.email 9id:bss:proposal:user4:email +proposal.user4.first_name 9id:bss:proposal:user4:firstName +proposal.user4.institution 9id:bss:proposal:user4:institution +proposal.user4.institution_id 9id:bss:proposal:user4:instId +proposal.user4.last_name 9id:bss:proposal:user4:lastName +proposal.user4.pi_flag 9id:bss:proposal:user4:piFlag OFF +proposal.user4.user_id 9id:bss:proposal:user4:userId +proposal.user5.badge_number 9id:bss:proposal:user5:badgeNumber +proposal.user5.email 9id:bss:proposal:user5:email +proposal.user5.first_name 9id:bss:proposal:user5:firstName +proposal.user5.institution 9id:bss:proposal:user5:institution +proposal.user5.institution_id 9id:bss:proposal:user5:instId +proposal.user5.last_name 9id:bss:proposal:user5:lastName +proposal.user5.pi_flag 9id:bss:proposal:user5:piFlag OFF +proposal.user5.user_id 9id:bss:proposal:user5:userId +proposal.user6.badge_number 9id:bss:proposal:user6:badgeNumber +proposal.user6.email 9id:bss:proposal:user6:email +proposal.user6.first_name 9id:bss:proposal:user6:firstName +proposal.user6.institution 9id:bss:proposal:user6:institution +proposal.user6.institution_id 9id:bss:proposal:user6:instId +proposal.user6.last_name 9id:bss:proposal:user6:lastName +proposal.user6.pi_flag 9id:bss:proposal:user6:piFlag OFF +proposal.user6.user_id 9id:bss:proposal:user6:userId +proposal.user7.badge_number 9id:bss:proposal:user7:badgeNumber +proposal.user7.email 9id:bss:proposal:user7:email +proposal.user7.first_name 9id:bss:proposal:user7:firstName +proposal.user7.institution 9id:bss:proposal:user7:institution +proposal.user7.institution_id 9id:bss:proposal:user7:instId +proposal.user7.last_name 9id:bss:proposal:user7:lastName +proposal.user7.pi_flag 9id:bss:proposal:user7:piFlag OFF +proposal.user7.user_id 9id:bss:proposal:user7:userId +proposal.user8.badge_number 9id:bss:proposal:user8:badgeNumber +proposal.user8.email 9id:bss:proposal:user8:email +proposal.user8.first_name 9id:bss:proposal:user8:firstName +proposal.user8.institution 9id:bss:proposal:user8:institution +proposal.user8.institution_id 9id:bss:proposal:user8:instId +proposal.user8.last_name 9id:bss:proposal:user8:lastName +proposal.user8.pi_flag 9id:bss:proposal:user8:piFlag OFF +proposal.user8.user_id 9id:bss:proposal:user8:userId +proposal.user9.badge_number 9id:bss:proposal:user9:badgeNumber +proposal.user9.email 9id:bss:proposal:user9:email +proposal.user9.first_name 9id:bss:proposal:user9:firstName +proposal.user9.institution 9id:bss:proposal:user9:institution +proposal.user9.institution_id 9id:bss:proposal:user9:instId +proposal.user9.last_name 9id:bss:proposal:user9:lastName +proposal.user9.pi_flag 9id:bss:proposal:user9:piFlag OFF +proposal.user9.user_id 9id:bss:proposal:user9:userId +proposal.user_badges 9id:bss:proposal:userBadges 86312,85283 +proposal.user_last_names 9id:bss:proposal:users Ilavsky,Okasinski +============================= ================================== ============================== diff --git a/docs/source/resources/bsv1.jpg b/docs/source/resources/bsv1.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/excel_plan_spreadsheet.jpg b/docs/source/resources/excel_plan_spreadsheet.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/excel_simple.jpg b/docs/source/resources/excel_simple.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/sample_example.jpg b/docs/source/resources/sample_example.jpg old mode 100755 new mode 100644 diff --git a/docs/source/resources/sample_example.xlsx b/docs/source/resources/sample_example.xlsx old mode 100755 new mode 100644 diff --git a/docs/source/resources/ui_id_entered.png b/docs/source/resources/ui_id_entered.png new file mode 100644 index 000000000..c4e5e724b Binary files /dev/null and b/docs/source/resources/ui_id_entered.png differ diff --git a/docs/source/resources/ui_initialized.png b/docs/source/resources/ui_initialized.png new file mode 100644 index 000000000..8bda52dc6 Binary files /dev/null and b/docs/source/resources/ui_initialized.png differ diff --git a/docs/source/resources/ui_updated.png b/docs/source/resources/ui_updated.png new file mode 100644 index 000000000..8d43f8db3 Binary files /dev/null and b/docs/source/resources/ui_updated.png differ diff --git a/docs/source/source/beamtime/_apsbss makedb.rst b/docs/source/source/beamtime/_apsbss makedb.rst new file mode 100644 index 000000000..014bd50d1 --- /dev/null +++ b/docs/source/source/beamtime/_apsbss makedb.rst @@ -0,0 +1,7 @@ + +apsbss: Create EPICS database +----------------------------- + +.. automodule:: apstools.beamtime.apsbss_makedb + :members: + diff --git a/docs/source/source/beamtime/_apsbss.rst b/docs/source/source/beamtime/_apsbss.rst new file mode 100644 index 000000000..fea8495cc --- /dev/null +++ b/docs/source/source/beamtime/_apsbss.rst @@ -0,0 +1,7 @@ + +apsbss: Command line application +-------------------------------- + +.. automodule:: apstools.beamtime.apsbss + :members: + diff --git a/docs/source/source/beamtime/_apsbss_ophyd.rst b/docs/source/source/beamtime/_apsbss_ophyd.rst new file mode 100644 index 000000000..df97ab423 --- /dev/null +++ b/docs/source/source/beamtime/_apsbss_ophyd.rst @@ -0,0 +1,7 @@ + +apsbss: Ophyd Device +-------------------- + +.. automodule:: apstools.beamtime.apsbss_ophyd + :members: + diff --git a/docs/source/source/beamtime/index.rst b/docs/source/source/beamtime/index.rst new file mode 100644 index 000000000..784b74b92 --- /dev/null +++ b/docs/source/source/beamtime/index.rst @@ -0,0 +1,14 @@ +.. _beamtime_source_docs: + +APS Proposal and ESAF Support +============================= + +Source code documentation of the +support for the APS Proposal and ESAF +(experiment safety approval form) databases. + +.. toctree:: + :maxdepth: 2 + :glob: + + _* diff --git a/docs/source/source/index.rst b/docs/source/source/index.rst index ffdf3e690..48e0d254c 100644 --- a/docs/source/source/index.rst +++ b/docs/source/source/index.rst @@ -8,4 +8,5 @@ API Documentation :glob: _* + beamtime/index synApps/index diff --git a/environment.yml b/environment.yml index df599f443..9ea47f937 100644 --- a/environment.yml +++ b/environment.yml @@ -1,19 +1,27 @@ name: apstoolsdoc channels: -- nsls2forge -- defaults + - defaults + - conda-forge + - aps-anl-tag + - nsls2forge dependencies: -- python=3.6 -- qt=5 -- pyqt=5 -- sphinx -- sphinx_rtd_theme -- bluesky -- ophyd -- prettytable -- pip: - - ipython-genutils==0.2.0 - - prompt-toolkit==1.0.15 - - sphinx-rtd-theme - - super-state-machine - - versioneer + - python>=3.6.0 + - aps-dm-api + - bluesky>=1.6.2 + - databroker>=1.0.6 + - h5py + - ophyd>=1.5.1 + - pip + - pyEpics>=3.4.2 + - pyqt=5 + - pyRestTable + - qt=5 + - spec2nexus>=2021.1.7 + - sphinx + - xlrd + - pip: + - ipython-genutils==0.2.0 + - prompt-toolkit==1.0.15 + - sphinx-rtd-theme + - super-state-machine + - versioneer diff --git a/setup.py b/setup.py index 810d1576e..f5541e1e7 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ """ -packaging setup for apstools +Packaging setup for apstools. """ # Always prefer setuptools over distutils @@ -16,6 +16,7 @@ __entry_points__ = { 'console_scripts': [ + 'apsbss = apstools.beamtime.apsbss:main', 'apstools_plan_catalog = apstools.examples:main', 'bluesky_snapshot = apstools.snapshot:snapshot_cli', 'bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui', diff --git a/tests/__main__.py b/tests/__main__.py index 154cd4429..65406de17 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -12,6 +12,7 @@ def suite(*args, **kw): + from tests import test_beamtime from tests import test_simple from tests import test_filewriter from tests import test_export_json @@ -25,6 +26,7 @@ def suite(*args, **kw): test_exceltable, test_commandlist, test_utils, + test_beamtime, ] test_suite = unittest.TestSuite() diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py new file mode 100644 index 000000000..5a9a9a7ae --- /dev/null +++ b/tests/test_beamtime.py @@ -0,0 +1,324 @@ + +""" +unit tests for beamtime info +""" + +import datetime +import os +import socket +import subprocess +import sys +import time +import unittest +import uuid + +_test_path = os.path.dirname(__file__) +_path = os.path.join(_test_path, '..') +if _path not in sys.path: + sys.path.insert(0, _path) + +from apstools.beamtime import apsbss, apsbss_makedb + +BSS_TEST_IOC_PREFIX = f"tst{uuid.uuid4().hex[:7]}:bss:" + + +def using_APS_workstation(): + hostname = socket.gethostname() + return hostname.lower().endswith(".aps.anl.gov") + + +def bss_IOC_available(): + import epics + # try connecting with one of the PVs in the database + cycle = epics.PV(f"{BSS_TEST_IOC_PREFIX}esaf:cycle") + cycle.wait_for_connection(timeout=2) + return cycle.connected + + +class Test_Beamtime(unittest.TestCase): + + def test_general(self): + self.assertEqual(apsbss.CONNECT_TIMEOUT, 5) + self.assertEqual(apsbss.POLL_INTERVAL, 0.01) + self.assertEqual( + apsbss.DM_APS_DB_WEB_SERVICE_URL, + "https://xraydtn01.xray.aps.anl.gov:11236") + self.assertIsNotNone(apsbss.api_bss) + self.assertIsNotNone(apsbss.api_esaf) + + def test_iso2datetime(self): + self.assertEqual( + apsbss.iso2datetime("2020-06-30 12:31:45.067890"), + datetime.datetime(2020, 6, 30, 12, 31, 45, 67890) + ) + + def test_not_at_aps(self): + self.assertTrue(True) # test something + if using_APS_workstation(): + return + + # do not try to test for fails using dm package, it has no timeout + + def test_only_at_aps(self): + self.assertTrue(True) # test something + if not using_APS_workstation(): + return + + runs = apsbss.listAllRuns() + self.assertGreater(len(runs), 1) + self.assertEqual(apsbss.getCurrentCycle(), runs[-1]) + self.assertEqual(apsbss.listRecentRuns()[0], runs[-1]) + + self.assertGreater(len(apsbss.listAllBeamlines()), 1) + + # TODO: test the other functions + # getCurrentEsafs + # getCurrentInfo + # getCurrentProposals + # getEsaf + # getProposal + # class DmRecordNotFound(Exception): ... + # class EsafNotFound(DmRecordNotFound): ... + # class ProposalNotFound(DmRecordNotFound): ... + + def test_printColumns(self): + from tests.common import Capture_stdout + with Capture_stdout() as received: + apsbss.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3) + self.assertEqual(len(received), 2) + self.assertEqual(received[0], "1 3 5 ") + self.assertEqual(received[1], "2 4 6 ") + + source = "0123456789" + self.assertEqual(apsbss.trim(source), source) + got = apsbss.trim(source, length=8) + self.assertNotEqual(got, source) + self.assertTrue(got.endswith("...")) + self.assertEqual(len(got), 8) + self.assertEqual(got, "01234...") + + +class Test_EPICS(unittest.TestCase): + + def setUp(self): + self.bss = None + self.manager = os.path.abspath(os.path.join( + os.path.dirname(apsbss.__file__), + "apsbss_ioc.sh" + )) + self.ioc_name = "test_apsbss" + cmd = f"{self.manager} restart {self.ioc_name} {BSS_TEST_IOC_PREFIX}" + self.ioc_process = subprocess.Popen( + cmd.encode().split(), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False) + time.sleep(.5) # allow the IOC to start + + def tearDown(self): + if self.bss is not None: + self.bss.destroy() + self.bss = None + if self.ioc_process is not None: + self.ioc_process = None + cmd = f"{self.manager} stop {self.ioc_name} {BSS_TEST_IOC_PREFIX}" + subprocess.Popen( + cmd.encode().split(), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False) + # self.ioc_process.communicate(cmd.encode().split()) + self.manager = None + + def test_ioc(self): + if not bss_IOC_available(): + return + + from apstools.beamtime import apsbss_ophyd as bio + + self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss") + self.bss.wait_for_connection(timeout=2) + self.assertTrue(self.bss.connected) + self.assertEqual(self.bss.esaf.title.get(), "") + self.assertEqual(self.bss.esaf.description.get(), "") + # self.assertEqual(self.bss.esaf.aps_cycle.get(), "") + + def test_EPICS(self): + from tests.common import Capture_stdout + + if not bss_IOC_available(): + return + + beamline = "9-ID-B,C" + cycle = "2019-3" + + with Capture_stdout(): + self.bss = apsbss.connect_epics(BSS_TEST_IOC_PREFIX) + self.assertTrue(self.bss.connected) + self.assertEqual(self.bss.esaf.aps_cycle.get(), "") + + self.bss.esaf.aps_cycle.put(cycle) + self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") + + if not using_APS_workstation(): + return + + # setup + with Capture_stdout(): + apsbss.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle) + self.assertNotEqual(self.bss.proposal.beamline_name.get(), "harpo") + self.assertEqual(self.bss.proposal.beamline_name.get(), beamline) + self.assertEqual(self.bss.esaf.aps_cycle.get(), cycle) + self.assertEqual(self.bss.esaf.sector.get(), beamline.split("-")[0]) + + # epicsUpdate + # Example ESAF on sector 9 + + # ====== ======== ========== ========== ==================== ================================= + # id status start end user(s) title + # ====== ======== ========== ========== ==================== ================================= + # 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS + # ====== ======== ========== ========== ==================== ================================= + + # ===== ====== =================== ==================== ======================================== + # id cycle date user(s) title + # ===== ====== =================== ==================== ======================================== + # 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r... + # ===== ====== =================== ==================== ======================================== + esaf_id = "226319" + proposal_id = "64629" + self.bss.esaf.aps_cycle.put("2019-2") + self.bss.esaf.esaf_id.put(esaf_id) + self.bss.proposal.proposal_id.put(proposal_id) + with Capture_stdout(): + apsbss.epicsUpdate(BSS_TEST_IOC_PREFIX) + self.assertEqual( + self.bss.esaf.title.get(), + "Commission 9ID and USAXS") + self.assertTrue( + self.bss.proposal.title.get().startswith( + "2019 National School on Neutron & X-r")) + + with Capture_stdout(): + apsbss.epicsClear(BSS_TEST_IOC_PREFIX) + self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "") + self.assertEqual(self.bss.esaf.title.get(), "") + self.assertEqual(self.bss.proposal.title.get(), "") + + +class Test_MakeDatabase(unittest.TestCase): + + def test_general(self): + from tests.common import Capture_stdout + with Capture_stdout() as db: + apsbss_makedb.main() + self.assertEqual(len(db), 345) + self.assertEqual(db[0], "#") + self.assertEqual(db[1], "# file: apsbss.db") + # randomly-selected spot checks + self.assertEqual(db[22], 'record(stringout, "$(P)esaf:id")') + self.assertEqual(db[128], ' field(ONAM, "ON")') + self.assertEqual(db[265], ' field(FTVL, "CHAR")') + + +class Test_ProgramCommands(unittest.TestCase): + + def setUp(self): + self.sys_argv0 = sys.argv[0] + sys.argv = [self.sys_argv0,] + + def tearDown(self): + sys.argv = [self.sys_argv0,] + + def test_no_options(self): + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertIsNone(args.subcommand) + + def test_beamlines(self): + sys.argv.append("beamlines") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "beamlines") + + def test_current(self): + sys.argv.append("current") + sys.argv.append("9-ID-B,C") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "current") + self.assertEqual(args.beamlineName, "9-ID-B,C") + + def test_cycles(self): + sys.argv.append("cycles") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "cycles") + + def test_esaf(self): + sys.argv.append("esaf") + sys.argv.append("12345") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "esaf") + self.assertEqual(args.esafId, 12345) + + def test_proposal(self): + sys.argv.append("proposal") + sys.argv.append("proposal_number_here") + sys.argv.append("1995-1") + sys.argv.append("my_beamline") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "proposal") + self.assertEqual(args.proposalId, "proposal_number_here") + self.assertEqual(args.cycle, "1995-1") + self.assertEqual(args.beamlineName, "my_beamline") + + def test_EPICS_clear(self): + sys.argv.append("clear") + sys.argv.append("bss:") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "clear") + self.assertEqual(args.prefix, "bss:") + + def test_EPICS_setup(self): + sys.argv.append("setup") + sys.argv.append("bss:") + sys.argv.append("my_beamline") + sys.argv.append("1995-1") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "setup") + self.assertEqual(args.prefix, "bss:") + self.assertEqual(args.beamlineName, "my_beamline") + self.assertEqual(args.cycle, "1995-1") + + def test_EPICS_update(self): + sys.argv.append("update") + sys.argv.append("bss:") + args = apsbss.get_options() + self.assertIsNotNone(args) + self.assertEqual(args.subcommand, "update") + self.assertEqual(args.prefix, "bss:") + + +def suite(*args, **kw): + test_list = [ + Test_Beamtime, + Test_EPICS, + Test_MakeDatabase, + Test_ProgramCommands, + ] + test_suite = unittest.TestSuite() + for test_case in test_list: + test_suite.addTest(unittest.makeSuite(test_case)) + return test_suite + + +if __name__ == "__main__": + runner=unittest.TextTestRunner() + runner.run(suite()) diff --git a/tests/test_filewriter.py b/tests/test_filewriter.py index bff08d121..9170573bd 100644 --- a/tests/test_filewriter.py +++ b/tests/test_filewriter.py @@ -242,7 +242,7 @@ def test_receiver_battery(self): callback.file_extension, apstools.filewriters.NEXUS_FILE_EXTENSION) - for plan_name, document_set in self.db.items(): + for plan_name in self.db: callback.clear() callback.file_path = self.tempdir self.assertIsNone(callback.uid, plan_name)