Skip to content

Commit

Permalink
First draft of advanced mirroring.
Browse files Browse the repository at this point in the history
  • Loading branch information
T-Nicholls committed Apr 29, 2019
1 parent 2c64607 commit e7dcc1e
Show file tree
Hide file tree
Showing 8 changed files with 577 additions and 206 deletions.
6 changes: 3 additions & 3 deletions ioc/SOFT-IOC.rst → ioc/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ from in the same way as any other PV, but for testing and debugging there is a
special method for setting them. This is done on the ATIP server object, inside
the server terminal (the one you ran `start-ioc` in initially). As arguments,
it takes the element's index in the ring (starting from 1, 0 is used to set on
the lattice), the field (possible fields are: ``'x_fofb_disabled',
the lattice), the field (possible element fields are: ``'x_fofb_disabled',
'x_sofb_disabled', 'y_fofb_disabled', 'y_sofb_disabled', 'h_fofb_disabled',
'h_sofb_disabled', 'v_fofb_disabled', 'v_sofb_disabled', 'error_sum',
'enabled', 'state', 'beam_current', feedback_status', 'bpm_enabled'``), and the
value to be set.
'enabled', 'state', 'offset'``; possible lattice fields are: ``'beam_current',
feedback_status', 'bpm_id', 'emittance_status'``), and the value to be set.

For example disabling SOFB on the first BPM, or reducing the beam current::

Expand Down
5 changes: 3 additions & 2 deletions ioc/atip_ioc_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import atip_server # noqa: E402
from softioc import builder, softioc # noqa: E402
from cothread.catools import caget, ca_nothing # noqa: E402
"""Error 402 is suppressed as we cannot import these modules at the top of the
file as they must be below the requires and the path editing.
"""Error 402 from pycodestyle is suppressed as we cannot import these modules
at the top of the file as they must be below the requires and the path editing.
"""

# Determine the ring mode
Expand Down Expand Up @@ -44,5 +44,6 @@
# Start the IOC.
builder.LoadDatabase()
softioc.iocInit()
server.monitor_mirrored_pvs()

softioc.interactive_ioc(globals())
164 changes: 119 additions & 45 deletions ioc/atip_server.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import csv
from warnings import warn

import atip
import numpy
import pytac
from cothread.catools import camonitor
from pytac.device import BasicDevice
from pytac.exceptions import HandleException, FieldException
from softioc import builder, device
from softioc import builder

from solution import camonitor_mask, caput_mask, summate, collate, transform


class ATIPServer(object):
Expand Down Expand Up @@ -48,10 +53,14 @@ def __init__(self, ring_mode, limits_csv=None, feedback_csv=None,
self._rb_only_records = []
self._feedback_records = {}
self._mirrored_records = {}
self._monitored_pvs = {}
print("Starting record creation.")
self._create_records(limits_csv)
if feedback_csv is not None:
self._create_feedback_records(feedback_csv)
self._all_in = {record.name: record for record in
self._in_records.keys() +
self._feedback_records.values()}
if mirror_csv is not None:
self._create_mirror_records(mirror_csv)
print("Finished creating all {0} records.".format(self.total_records))
Expand All @@ -77,8 +86,6 @@ def update_pvs(self):
value = self.lattice[index-1].get_value(field, units=pytac.ENG,
data_source=pytac.SIM)
rb_record.set(value)
if rb_record.name in self._mirrored_records:
self._mirrored_records[rb_record.name].set(value)

def _create_records(self, limits_csv):
"""Create all the standard records from both lattice and element Pytac
Expand Down Expand Up @@ -168,7 +175,7 @@ def on_update(value, name=set_pv):
initial_value=value,
on_update=on_update)
self._out_records[out_record.name] = in_record
# Now for lattice fields
# Now for lattice fields.
lat_fields = self.lattice.get_fields()
for field in set(lat_fields[pytac.LIVE]) & set(lat_fields[pytac.SIM]):
# Ignore basic devices as they do not have PVs.
Expand All @@ -183,6 +190,22 @@ def on_update(value, name=set_pv):
self._rb_only_records.append(in_record)
print("~*~*Woah, we're halfway there, Wo-oah...*~*~")

def _on_update(self, name, value):
"""The callback function passed to out records, it is called after
successful record processing has been completed. It updates the out
record's corresponding in record with the value that has been set and
then sets the value to the centralised Pytac lattice.
Args:
name (str): The name of record object that has just been set to.
value (number): The value that has just been set to the record.
"""
in_record = self._out_records[name]
index, field = self._in_records[in_record]
self.lattice[index-1].set_value(field, value, units=pytac.ENG,
data_source=pytac.SIM)
in_record.set(value)

def _create_feedback_records(self, feedback_csv):
"""Create all the feedback records from the .csv file at the location
passed, see create_csv.py for more information.
Expand All @@ -193,65 +216,116 @@ def _create_feedback_records(self, feedback_csv):
"""
csv_reader = csv.DictReader(open(feedback_csv))
for line in csv_reader:
prefix, pv = line['pv'].split(':', 1)
prefix, suffix = line['pv'].split(':', 1)
builder.SetDeviceName(prefix)
in_record = builder.longIn(pv, initial_value=int(line['value']))
in_record = builder.longIn(suffix,
initial_value=int(line['value']))
self._feedback_records[(int(line['index']),
line['field'])] = in_record

# Storage ring electron BPMs enabled
# Special case: since cannot currently create waveform records via CSV,
# create by hand and add to list of feedback records
N_BPM = len(self.lattice.get_elements('BPM'))
# Special case: BPM ID for the x axis of beam position plot, since we
# cannot currently create Waveform records via CSV.
bpm_ids = [int(pv[2:4]) + 0.1 * int(pv[14:16]) for pv in
self.lattice.get_element_pv_names('BPM', 'x', pytac.RB)]
builder.SetDeviceName("SR-DI-EBPM-01")
bpm_enabled_record = builder.Waveform("ENABLED", NELM=N_BPM,
initial_value=[0] * N_BPM)
self._feedback_records[(0, "bpm_enabled")] = bpm_enabled_record
bpm_id_record = builder.Waveform("BPMID", NELM=len(bpm_ids),
initial_value=bpm_ids)
self._feedback_records[(0, "bpm_id")] = bpm_id_record
# Special case: EMIT STATUS for the vertical emittance feedback, since
# we cannot currently create mbbIn records via CSV.
builder.SetDeviceName("SR-DI-EMIT-01")
emit_status_record = builder.mbbIn("STATUS", initial_value=0, ZRVL=0,
ZRST="Successful", PINI="YES")
self._feedback_records[(0, "emittance_status")] = emit_status_record

def _create_mirror_records(self, mirror_csv):
all_in_records = (self._in_records.keys() +
self._feedback_records.values())
record_names = {rec.name: rec for rec in all_in_records}
csv_reader = csv.DictReader(open(mirror_csv))
for line in csv_reader:
prefix, pv = line['mirror'].split(':', 1)
# Parse arguments.
input_pvs = line['in'].split(', ')
if (len(input_pvs) > 1) and (line['mirror type'] in ['basic',
'inverse']):
raise IndexError("Transformation and basic mirror types take "
"only one input PV.")
elif (len(input_pvs) < 2) and (line['mirror type'] in ['collate',
'summate']):
raise IndexError("collation and summation mirror types take at"
" least two input PVs.")
if line['monitor'] == '':
monitor = input_pvs
else:
monitor = line['monitor'].split(', ')
# Create output record.
prefix, suffix = line['out'].split(':', 1)
builder.SetDeviceName(prefix)
if isinstance(record_names[line['original']]._RecordWrapper__device,
device.ai):
mirror = builder.aIn(pv, initial_value=float(line['value']))
elif isinstance(record_names[line['original']]._RecordWrapper__device,
device.longin):
mirror = builder.aIn(pv, initial_value=float(line['value']))
if line['record type'] == '':
output_record = self._all_in[line['out']]
elif line['record type'] == 'caput':
output_record = caput_mask(line['out'])
elif line['record type'] == 'aIn':
value = float(line['value'])
output_record = builder.aIn(suffix, initial_value=value)
elif line['record type'] == 'longIn':
value = int(line['value'])
output_record = builder.longIn(suffix, initial_value=value)
elif line['record type'] == 'Waveform':
value = numpy.asarray(line['value'][1:-1].split(', '),
dtype=float)
output_record = builder.Waveform(suffix, initial_value=value)
else:
raise TypeError("Type {0} doesn't currently support mirroring,"
" please only mirror aIn and longIn records."
.format(type(line['original']._RecordWrapper__device)))
self._mirrored_records[line['original']] = mirror
raise TypeError("Record type {0} doesn't currently support "
"mirroring, please enter 'aIn', 'longIn', or "
"'Waveform'.".format(line['record type']))
# Update the mirror dictionary.
for pv in monitor:
if pv not in self._mirrored_records:
self._mirrored_records[pv] = []
if line['mirror type'] == 'basic':
self._mirrored_records[monitor[0]].append(output_record)
elif line['mirror type'] == 'inverse':
# Other transformation types are not yet supported.
transformation = transform(numpy.invert, output_record)
self._mirrored_records[monitor[0]].append(transformation)
elif line['mirror type'] == 'summate':
summation_object = summate(input_pvs, output_record)
for pv in monitor:
self._mirrored_records[pv].append(summation_object)
elif line['mirror type'] == 'collate':
collation_object = collate(input_pvs, output_record)
for pv in monitor:
self._mirrored_records[pv].append(collation_object)
else:
raise TypeError("Mirror type {0} is not currently supported, "
"please enter 'basic', 'summate', 'collate' or"
" 'inverse'.".format(line['mirror type']))

def _on_update(self, name, value):
"""The callback function passed to out records, it is called after
successful record processing has been completed. It updates the out
record's corresponding in record with the value that has been set and
then sets the value to the centralised Pytac lattice.
def monitor_mirrored_pvs(self):
for pv, output in self._mirrored_records.items():
mask = camonitor_mask(output)
try:
self._monitored_pvs[pv] = camonitor(pv, mask.callback)
except ImportError as e:
warn(e)

Args:
name (str): The name of record object that has just been set to.
value (number): The value that has just been set to the record.
"""
in_record = self._out_records[name]
index, field = self._in_records[in_record]
self.lattice[index-1].set_value(field, value, units=pytac.ENG,
data_source=pytac.SIM)
in_record.set(value)
if in_record.name in self._mirrored_records:
self._mirrored_records[in_record.name].set(value)
def refresh_pv(self, pv_name):
try:
record = self._all_in[pv_name]
except KeyError:
raise ValueError("{0} is not an in record or was not created by "
"this server.".format(pv_name))
else:
record.set(record.get())

def set_feedback_record(self, index, field, value):
"""Set a value to the feedback in records, possible fields are:
"""Set a value to the feedback in records.
possible element fields are:
['x_fofb_disabled', 'x_sofb_disabled', 'y_fofb_disabled',
'y_sofb_disabled', 'h_fofb_disabled', 'h_sofb_disabled',
'v_fofb_disabled', 'v_sofb_disabled', 'error_sum', 'enabled',
'state', 'beam_current', feedback_status', 'bpm_enabled']
'state', 'offset']
possible lattice fields are:
['beam_current', 'feedback_status', 'bpm_id', 'emittance_status']
Args:
index (int): The index of the element on which to set the value;
Expand Down
82 changes: 69 additions & 13 deletions ioc/create_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ def generate_feedback_pvs():
elem.get_pv_name('enabled', pytac.RB), 1))
# Add elements for Tune Feedback
elif elem in tune_quad_elements:
# We must build the PV name because there is no field for OFFSET1
pv_stem = elem.get_device("b1").name
data.append((elem.index, "OFFSET1",
"{}:OFFSET1".format(pv_stem), 0))
data.append((elem.index, "offset",
elem.get_device("b1").name + ':OFFSET1', 0))

return data

Expand All @@ -85,21 +83,79 @@ def generate_pv_limits():


def generate_mirrored_pvs():
"""
monitor: pv(s) to be monitored, on change mirror is updated; if '' then
the input pv(s) are monitored.
in: pv(s) to read from, if multiple then pvs are separated by a comma and
one space.
out: single pv to output to, if a 'record type' is spcified then a new
record will be created and so must not exist already.
value: the inital value of the output record.
record type: the type of output record to create, only 'aIn', 'longIn',
'Waveform' types are currently supported; if '' then output to an
existing in record already created in ATIPServer, 'caput' is also a
special case it creates a mask for cothread.catools.caput calling
set(value) on this mask will call caput with the output pv and the
passed value.
mirror type: type of mirroring to apply:
- basic: set the value of the input record to the output record.
- summate: sum the values of the input records and set the result to
the output record.
- collate: create a Waveform record from the values of the input pvs.
- transform: apply the specified transformation function to the value
of the input record and set the result to the output record. N.B.
the only transformation type currently supported is 'inverse'.
"""
lattice = atip.utils.loader()
data = [("original", "mirror", "value"),
("SR23C-DI-TMBF-01:X:TUNE:TUNE", "SR23C-DI-TMBF-01:TUNE:TUNE",
lattice.get_value("tune_x", pytac.RB)),
("SR23C-DI-TMBF-01:Y:TUNE:TUNE", "SR23C-DI-TMBF-02:TUNE:TUNE",
lattice.get_value("tune_y", pytac.RB)),
("SR-DI-EMIT-01:HEMIT", "SR-DI-EMIT-01:HEMIT_MEAN",
lattice.get_value("emittance_x", pytac.RB)),
("SR-DI-EMIT-01:VEMIT", "SR-DI-EMIT-01:VEMIT_MEAN",
lattice.get_value("emittance_y", pytac.RB))]
data = [("monitor", "in", "out", "value", "record type", "mirror type")]
# Tune PV aliases.
tune = [lattice.get_value('tune_x', pytac.RB, data_source=pytac.SIM),
lattice.get_value('tune_y', pytac.RB, data_source=pytac.SIM)]
data.append(('', 'SR23C-DI-TMBF-01:X:TUNE:TUNE',
'SR23C-DI-TMBF-01:TUNE:TUNE', tune[0], 'aIn', 'basic'))
data.append(('', 'SR23C-DI-TMBF-01:Y:TUNE:TUNE',
'SR23C-DI-TMBF-02:TUNE:TUNE', tune[1], 'aIn', 'basic'))
# Combined emittance and average emittance PVs.
emit = [lattice.get_value('emittance_x', pytac.RB, data_source=pytac.SIM),
lattice.get_value('emittance_y', pytac.RB, data_source=pytac.SIM)]
data.append(('', 'SR-DI-EMIT-01:HEMIT', 'SR-DI-EMIT-01:HEMIT_MEAN',
emit[0], 'aIn', 'basic'))
data.append(('', 'SR-DI-EMIT-01:VEMIT', 'SR-DI-EMIT-01:VEMIT_MEAN',
emit[1], 'aIn', 'basic'))
data.append(('', 'SR-DI-EMIT-01:HEMIT, SR-DI-EMIT-01:VEMIT',
'SR-DI-EMIT-01:EMITTANCE', sum(emit), 'aIn', 'summate'))
# Electron BPMs enabled.
bpm_enabled_pvs = lattice.get_element_pv_names('BPM', 'enabled', pytac.RB)
data.append(('', ', '.join(bpm_enabled_pvs), 'EBPM-ENABLED:INTERIM',
[0] * len(bpm_enabled_pvs), 'Waveform', 'collate'))
data.append(('', 'EBPM-ENABLED:INTERIM', 'SR-DI-EBPM-01:ENABLED',
[0] * len(bpm_enabled_pvs), 'Waveform', 'inverse'))
# BPM x positions for display on diagnostics screen.
bpm_x_pvs = lattice.get_element_pv_names('BPM', 'x', pytac.RB)
data.append(('', ', '.join(bpm_x_pvs), 'SR-DI-EBPM-01:SA:X',
[0] * len(bpm_x_pvs), 'Waveform', 'collate'))
# BPM y positions for display on diagnostics screen.
bpm_y_pvs = lattice.get_element_pv_names('BPM', 'y', pytac.RB)
data.append(('', ', '.join(bpm_y_pvs), 'SR-DI-EBPM-01:SA:Y',
[0] * len(bpm_y_pvs), 'Waveform', 'collate'))
# Offset PV for quadrupoles in tune feedback.
tune_pvs = []
offset_pvs = []
for family in ['Q1D', 'Q2D', 'Q3D', 'Q3B', 'Q2B', 'Q1B']:
tune_pvs.extend(lattice.get_element_pv_names(family, 'b1', pytac.SP))
for pv in tune_pvs:
offset_pvs.append('SR-CS-TFB-01:{0}{1}{2}:I'.format(pv[2:4], pv[9:12],
pv[13:15]))
for offset_pv, tune_pv in zip(offset_pvs, tune_pvs):
data.append((offset_pv, ', '.join([offset_pv, tune_pv]), tune_pv, 0.0,
'caput', 'summate'))
return data


def write_data_to_file(data, filename):
# Write the collected data to the .csv file.
if not filename.endswith('.csv'):
filename += '.csv'
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, filename), "wb") as file:
csv_writer = csv.writer(file)
Expand Down

0 comments on commit e7dcc1e

Please sign in to comment.