Ramp Tables¶
The Ramp class provides convenient interface for reading and writing ramp tables. The multi-device RampGroup provides batched 2D-array access.
Overview¶
Each ramp slot contains 64 points stored as raw bytes in the SETTING property. Each point is 4 bytes little-endian: a signed int16 value followed by a signed int16 time (clock ticks).
byte[0:1] = value (int16 LE) -- F(t) amplitude
byte[2:3] = time (int16 LE) -- delta time (clock ticks)
The total slot size is 256 bytes. Ramp slots are indexed starting at 0, and can be manipulated using SETTING property SETTING{N*256:256}.RAW for ramp N.
Array Types¶
.values and .times must be numeric numpy arrays (int or float dtypes). Non-numeric types (strings, booleans, complex, objects) are rejected with TypeError on assignment. All arrays are stored internally as float64.
Value Scaling¶
Values are converted between raw int16 and engineering units. The standard two-stage ACNET transform chain is:
Forward: engineering = common_scale(primary_scale(raw))
Inverse: raw = primary_unscale(common_unscale(engineering))
There are two ways to define the scaling:
- Set
scalerto aScalerinstance (recommended for standard ACNET transforms). - Override transform classmethods for custom/non-standard transforms.
Pre-defined subclasses for common elements:
| Class | Card | Examples | Primary (p_index) | Common (c_index) |
|---|---|---|---|---|
BoosterHVRamp |
C473 | B:HS23T, B:SSS23T, B:SXS23T | raw / 3276.8 (2) | primary × 4.0 (6, C1=4.0, C2=1.0) |
BoosterQRamp |
C473 | B:QS23T | raw / 3276.8 (2) | primary × 6.5 (6, C1=6.5, C2=1.0) |
RecyclerQRamp |
C453 | R:QT606T | raw / 3276.8 (2) | primary × 2.0 (6, C1=2.0, C2=1.0) |
RecyclerSRamp |
C453 | R:S202T | raw / 3276.8 (2) | primary × 1.2 (6, C1=12.0, C2=10.0) |
RecyclerSCRamp |
C475 | R:SC319T | raw / 3276.8 (2) | primary × 1.2000000477 (6, C1=1.2000000477, C2=1.0) |
RecyclerHVSQRamp |
C453 | R:H626T, R:SQ410T | raw / 3276.8 (2) | primary × 1.2 (6, C1=12.0, C2=10.0) |
Time Scaling¶
Raw times on the wire are clock ticks. The card's update_rate_hz determines the tick period. .times stores delta times between consecutive points in microseconds:
Forward: time_us = raw_ticks * (1e6 / update_rate_hz)
Inverse: raw_ticks = round(time_us * update_rate_hz / 1e6)
Different card types have different update rates:
| Card Class | Type | Update Rate | Tick Period |
|---|---|---|---|
| 453 | CAMAC | 720 Hz fixed | 1389 µs |
| 465/466 | CAMAC | 1 / 5 / 10 KHz | 1000 µs / 200 µs / 100 µs |
| 473 | CAMAC | 100 KHz fixed | 10 µs |
Pre-defined subclasses time scaling:
| Class | update_rate_hz |
Tick Period | Max Time |
|---|---|---|---|
Ramp (default) |
10,000 Hz | 100 µs | (none) |
BoosterHVRamp |
100,000 Hz | 10 µs | 66,660 µs (~one 15 Hz cycle) |
BoosterQRamp |
100,000 Hz | 10 µs | 66,660 µs (~one 15 Hz cycle) |
RecyclerQRamp |
720 Hz | 1,389 µs | (none) |
RecyclerSRamp |
720 Hz | 1,389 µs | (none) |
RecyclerSCRamp |
100,000 Hz | 10 µs | (none) |
RecyclerHVSQRamp |
720 Hz | 1,389 µs | (none) |
Cumulative Times¶
The .cumtimes property provides a convenience view of times as absolute (cumulative) microseconds since ramp start, rather than deltas between points:
ramp = BoosterHVRamp.read("B:HS23T")
ramp.times # [0, 100, 200, 300, ...] deltas between points
ramp.cumtimes # [0, 100, 300, 600, ...] absolute times since start
Setting .cumtimes automatically converts to deltas and stores in .times:
ramp.cumtimes = np.array([0, 100, 300, 600, ...])
ramp.times # [0, 100, 200, 300, ...] (computed via np.diff)
For RampGroup, .cumtimes operates column-wise (per device).
Read/Write¶
from pacsys import BoosterHVRamp
# Read - stores device and slot on the ramp
ramp = BoosterHVRamp.read("B:HS23T", slot=0)
print(ramp.values) # float64 array, Amps
print(ramp.times) # float64 array, delta microseconds
print(ramp.cumtimes) # float64 array, absolute microseconds
ramp.device # "B:HS23T"
ramp.slot # 0
# Modify
ramp.values[:8] = [1.0, 2.0, 3.0, 4.0, 4.0, 3.0, 2.0, 1.0] # Amps
ramp.times[:8] = [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000] # delta microseconds
# Or set absolute times (automatically converted to deltas)
ramp.cumtimes = np.array([0, 10000, 30000, 60000, ...])
# Write back (uses stored device/slot)
ramp.write()
# Or write to a different device/slot
ramp.write(device="B:HS24T", slot=1)
Context Manager¶
The modify() context manager handles read-modify-write automatically:
from pacsys import BoosterHVRamp
with BoosterHVRamp.modify("B:HS23T", slot=1) as ramp:
ramp.values[0] += 1.0 # bump first point by 1 Amp
Ramp state is read on context entrance and changes are written on context exit; nothing is written if no changes were made or an exception occurs.
Batched Read/Write¶
Read or write multiple devices in a single backend call using read_ramps() / write_ramps() or the Ramp.read_many() classmethod:
from pacsys import BoosterHVRamp, read_ramps, write_ramps
# Batched read - single get_many call
ramps = BoosterHVRamp.read_many(["B:HS23T", "B:HS24T", "B:HS25T"], slot=0)
# or equivalently:
ramps = read_ramps(BoosterHVRamp, ["B:HS23T", "B:HS24T", "B:HS25T"], slot=0)
for ramp in ramps:
print(f"{ramp.device}: {ramp.values[0]:.2f} A")
# Batched write - single write_many call
write_ramps(ramps)
write_ramps accepts flexible inputs: a single Ramp, a list[Ramp], a RampGroup, or a mixed list. All are flattened into one write_many call:
write_ramps(ramp) # single Ramp
write_ramps([ramp1, ramp2]) # list[Ramp]
write_ramps(group) # RampGroup
write_ramps([group1, group2, ramp3]) # mixed - flattened
write_ramps(ramps, slot=2) # override slot for all
RampGroup (2D Array Semantics)¶
RampGroup stores ramp data for multiple devices as 2D numpy arrays with shape (64, N_devices). Axis 0 is the point index, axis 1 is the device.
from pacsys import BoosterHVRampGroup
group = BoosterHVRampGroup.read(["B:HS23T", "B:HS24T", "B:HS25T"], slot=0)
group.values # shape (64, 3) float64 - engineering units
group.times # shape (64, 3) float64 - delta microseconds
group.cumtimes # shape (64, 3) float64 - absolute microseconds (property)
group.devices # ["B:HS23T", "B:HS24T", "B:HS25T"]
2D Array Operations¶
group.values[5] += 0.5 # bump point 5 for all devices
group.times += 100 # shift all times by 100 us
group.values += 0.5 # broadcast across all points and devices
Device Indexing¶
group['B:HS23T'] returns a view-backed Ramp - mutations propagate both ways:
ramp = group['B:HS23T']
ramp.values[0] += 1.0 # also modifies group.values[0, 0]
group.values[0, 0] = 5.0 # also visible via ramp.values[0]
Writing¶
# Write to stored devices/slot
group.write()
# Override targets
group.write(devices=["B:OTHER1", "B:OTHER2", "B:OTHER3"], slot=1)
Group Context Manager (read-modify-write)¶
The modify() context manager reads on entry and writes only changed devices on exit:
with BoosterHVRampGroup.modify(["B:HS23T", "B:HS24T"], slot=0) as group:
group.values[10] += 0.5 # bump point 10 for all devices
# writes on exit if changed; raises RuntimeError on partial failure
Serialization¶
Ramps and ramp groups support round-trippable JSON serialization via to_dict() / from_dict():
import json
# Ramp → dict → JSON → dict → Ramp
ramp = BoosterHVRamp.read("B:HS23T", slot=0)
d = ramp.to_dict()
json_str = json.dumps(d) # fully JSON-safe (plain lists, strings, ints)
restored = Ramp.from_dict(json.loads(json_str)) # dispatches to BoosterHVRamp
assert type(restored) is BoosterHVRamp
# Same for groups
group = BoosterHVRampGroup.read(["B:HS23T", "B:HS24T"])
d = group.to_dict()
restored_group = RampGroup.from_dict(json.loads(json.dumps(d)))
The "type" key in the dict identifies the subclass. Ramp.from_dict() and RampGroup.from_dict() dispatch to the correct built-in subclass automatically.
Custom subclasses are not in the registry — calling Ramp.from_dict() with an unknown type raises ValueError. Instead, call from_dict() on the subclass directly:
class MyRamp(Ramp):
...
d = my_ramp.to_dict() # works — includes "type": "MyRamp"
Ramp.from_dict(d) # raises ValueError
MyRamp.from_dict(d) # works — bypasses registry
Custom Machine Types¶
Info
When the new DevDB service is deployed in a more production-ready state, device property scaling will be automatic for known channels. For now, this step is kept manual.
Using Scaler (recommended)¶
Set the scaler class variable to a Scaler instance with the device's scaling parameters from the database (p_index, c_index, and constants). This is what BoosterHVRamp uses:
from pacsys import Ramp, Scaler
class BoosterHVRamp(Ramp):
update_rate_hz = 100_000 # 473 card: 100 KHz fixed
max_value = 1000.0
max_time = 66_660.0
scaler = Scaler(p_index=2, c_index=6, constants=(4.0, 1.0), input_len=2)
ramp = BoosterHVRamp.read("B:HS23T", slot=0)
The scaling parameters can be found in the device database or looked up via DevDB:
from pacsys import Scaler
with pacsys.devdb() as db:
info = db.get_device_info(["B:HS23T"])
prop = info["B:HS23T"].setting
scaler = Scaler.from_property_info(prop, input_len=2)
print(scaler) # Scaler(p_index=2, c_index=6, constants=(4.0, 1.0), input_len=2)
See Scaling for details on Scaler, transform indices, and supported operations.
Manual Transforms¶
For non-standard scaling (e.g., nonlinear, lookup tables, or transforms not covered by the Scaler), subclass Ramp and override the four transform classmethods:
from pacsys import Ramp
class MainInjectorRamp(Ramp):
update_rate_hz = 5000 # 5 KHz card (200 us/tick)
max_value = 500.0 # optional validation bound (engineering units)
max_time = 1_000_000 # optional validation bound on cumulative time (microseconds)
@classmethod
def primary_transform(cls, raw):
return raw / 1638.4
@classmethod
def common_transform(cls, primary):
return primary * 2.0
@classmethod
def inverse_common_transform(cls, common):
return common / 2.0
@classmethod
def inverse_primary_transform(cls, primary):
return primary * 1638.4
ramp = MainInjectorRamp.read("MI:DEVICE", slot=0)
Transforms can be nonlinear (e.g., polynomial, lookup table).
Custom RampGroup¶
Ramp groups are subclassed by providing the base ramp class, without needing to duplicate transform code.
from pacsys.ramp import RampGroup
class MainInjectorRampGroup(RampGroup):
base = MainInjectorRamp
group = MainInjectorRampGroup.read(["MI:DEV1", "MI:DEV2"], slot=0)
Raw Bytes¶
For low-level access, from_bytes() and to_bytes() handle the binary encoding:
from pacsys import BoosterHVRamp
raw = b"\x00" * 256 # 64 zero points
ramp = BoosterHVRamp.from_bytes(raw)
# Serialize back
raw_out = ramp.to_bytes()
Error Handling¶
from pacsys import BoosterHVRamp
from pacsys.errors import DeviceError
try:
ramp = BoosterHVRamp.read("B:BADDEV", slot=0)
except DeviceError as e:
print(f"Read failed: {e}")
Type errors are raised immediately on assignment if a non-numeric dtype is used:
ramp.values = np.array(["a"] * 64) # TypeError: must be numeric
ramp.times = np.zeros(64, dtype=bool) # TypeError: must be numeric
Array shape is enforced on every assignment — arrays must be exactly 64 elements, 1-D:
ramp.values = np.zeros(65) # ValueError: Expected 64 values, got 65
ramp.values = np.zeros((64, 2)) # ValueError: values must be 1-D
Slot index is validated on all read/write/modify operations (must be int, 0..15):
BoosterHVRamp.read("B:HS23T", slot=-1) # ValueError: slot must be 0..15
BoosterHVRamp.read("B:HS23T", slot=True) # TypeError: slot must be an int
Validation errors (values exceeding max_value or max_time) are raised during to_bytes() / write():
ramp.values[0] = 1500.0 # exceeds BoosterHVRamp.max_value (1000.0)
ramp.to_bytes() # raises ValueError
Display¶
ramp = BoosterHVRamp.read("B:HS23T")
print(repr(ramp)) # BoosterHVRamp(8/64 active points)
print(ramp)
# BoosterHVRamp (64 points):
# [ 0] t= 0.0us value=1.2345
# [ 1] t= 2400.0us value=2.3456
# ...
Ramp Card Hardware details¶
C453 -- Quad Ramp Controller¶
The 453 class CAMAC ramp card produces four output waveforms in response to a TCLK event. There are 32 defined interrupt levels, each triggered by the OR of up to 8 TCLK events.
For each interrupt level the output waveform is:
Where:
- sf1, sf2, sf3 -- scale factors (-128 to +127.9)
- m1, m2, m3 -- raw MDAT readings / 256
- F(t) -- interpolated function of time which is initiated by the 'or' of up to 8 TCLK events (the ramp table
Rampmanipulates) - g(M1), h(M2) -- interpolated functions of selected MDAT parameters
Update frequency is 720Hz. Up to 15 ramp slots can be defined. The outputs are 12 bits +/- 10.000V. See references for more details.
C465/C466 -- Waveform Generator¶
The 465 class CAMAC ramp card produces a single output waveform in response to a TCLK event. There are 32 defined interrupt levels, each triggered by the OR of up to 8 TCLK events.
For each interrupt level the output waveform is:
Where:
- sf1, sf2, sf3 -- scale factors (-128 to +127.9 bipolar, 0 to +255.9 unipolar)
- m1, m2, m3 -- raw MDAT readings / 256
- F(t) -- interpolated function of time which is initiated by the 'or' of up to 8 TCLK events (the ramp table
Rampmanipulates) - g(M1), h(M2) -- interpolated functions of selected MDAT parameters
Update frequency is configurable between 1/5/10 kHz. Up to 15 ramp slots can be defined. The outputs are (16 bits, +/- 10.0V) (C465 and C467) and (16 bits, 0 - 10.0V) (C466 and C468). There are also differences in status reporting. See references for more details.
C473/C475 -- Quad Ramp Controller¶
The 473 CAMAC ramp card has a fixed 100 KHz update rate (10 µs tick period).
The output waveform is:
Where:
- sf1, sf2, and sf3 are constant scale factors having a range of -128.0 to +127.9
- f(t) is an interpolated function of time which is initiated by a TCLK event
- offset is a constant offset having a range of -32768 to +32767
- M1 and M2 are variable values received via MDAT
- g(M1) and h(M2) are interpolated functions of M1 and M2, respectively
The output functions of all four channels share a common trigger. Each channel has an independent delay, programmable from 0 to 65535 µsec, between the TCLK trigger event and the start of the output functions.
Note: Although the shortest programmable delay is 0 µsec, at least 30 µsec (C473) or 100 µsec (C475) must be allowed for the processor to service the trigger interrupt. The C473/C475 will enforce the minimum delay.
See references for more details and configuration.