Skip to main content

Pure-python library for Fermilab control system

Project description

pacsys

Pure-Python library for Fermilab's control system.

Tests Documentation License: GPL-3.0 Python 3.10+

About

ACNET (Accelerator Control NETwork) is the control system used at Fermilab's particle accelerators. PACSys provides a simple Python interface to interact with ACNET data without needing to understand the underlying protocols.

Features

  • Read/Write/Stream any ACNET data types with synchronous or async APIs
  • Multiple backends to connect to DPM, DMQ, and ACL
  • Full DRF3 parser for data requests with automatic conversion
  • Utilities for device database, SSH tunneling, and more
  • Command-line tools like in EPICS - acget, acput, acmonitor, acinfo
  • MCP server for AI agent integration (Claude Code, etc.) with supervised writes

Installation

pip install pacsys

Device API (recommended)

import pacsys
from pacsys import Device, Verify, KerberosAuth

# Create a device -- DRF is validated immediately
dev = Device("M:OUTTMP")

# Read different properties
temperature = dev.read()               # READING (scaled value)
setpoint = dev.setting()               # SETTING property
is_on = dev.status(field="on")         # STATUS field ON
alarm = dev.analog_alarm()             # ANALOG alarm

# Full reading with metadata
reading = dev.get()
print(f"{reading.value} {reading.units}")  # e.g. "72.5 DegF"

# Write with automatic readback verification
result = dev.write(72.5, verify=Verify(tolerance=0.5))
assert result.verified

# Control commands with shortcuts
dev.on()
dev.off()
dev.reset()

# Device database metadata (scaling, limits, units)
info = dev.info()
print(info.description)                # "Outside temperature"
print(info.reading.common_units)       # "DegF"
print(info.reading.min_val)            # 0.0

# Block until next reading arrives
reading = dev.with_event("p,1000").await_next(timeout=5)
print(reading.value)

# Stream data
with dev.with_event("p,1000").subscribe() as stream:
    for reading, handle in stream.readings(timeout=10):
        print(reading.value)

# Immutable -- modifications return new instances
periodic_dev = dev.with_event("p,1000")
sliced_dev = dev.with_range(0, 10)

Backend API

import time
import pacsys

# Read a device value through global backend
temperature = pacsys.read("M:OUTTMP")
print(f"Temperature: {temperature}")

# Stream real-time data through global backend
with pacsys.subscribe(["M:OUTTMP@p,1000"]) as stream:
    for reading, handle in stream.readings(timeout=30):
        print(f"{reading.name}: {reading.value}")

# Stream with callback dispatch mode through dedicated DPM instance
# WORKER (default): callbacks on dedicated worker thread, protects event loop
# DIRECT: callbacks inline on reactor thread (lower latency)
with pacsys.dpm(dispatch_mode=pacsys.DispatchMode.DIRECT) as backend:
    handle = backend.subscribe(
        ["M:OUTTMP@p,1000"],
        callback=lambda r, h: print(r.value),
    )
    time.sleep(10)
    handle.stop()

# Write through authenticated DPM instance (requires kerberos ticket)
with pacsys.dpm(auth=pacsys.KerberosAuth(), role="testing") as backend:
    backend.write("Z:ACLTST", 72.5)

Async capabilities

Native async versions with same API surface.

import pacsys.aio as aio

# Module-level API (mirrors pacsys.read, pacsys.get, etc.)
value = await aio.read("M:OUTTMP")
reading = await aio.get("M:OUTTMP")

# Explicit async backend
async with aio.dpm(auth=pacsys.KerberosAuth()) as backend:
    await backend.write("Z:ACLTST", 72.5)

# Async streaming
async with await backend.subscribe(["M:OUTTMP@p,1000"]) as stream:
    async for reading, handle in stream.readings(timeout=30):
        print(f"{reading.name}: {reading.value}")

# AsyncDevice
from pacsys.aio import AsyncDevice

dev = AsyncDevice("M:OUTTMP", backend=backend)
temp = await dev.read()
await dev.on()

SSH Utilities

Port tunneling, SFTP, and interactive processes over multi-hop SSH.

import pacsys

# Execute commands with automatic Kerberos auth
with pacsys.ssh(["jump.fnal.gov", "target.fnal.gov"]) as ssh:
    result = ssh.exec("hostname")
    print(result.stdout) # target

# ACL can be run on the fly - beam switch, DB, etc.
with pacsys.ssh("clx01.fnal.gov") as ssh:
    result = ssh.acl("read M:OUTTMP") # "M:OUTTMP       =  72.500 DegF"

MCP Server (AI Agent Integration)

Expose pacsys as an MCP server for AI agents like Claude Code. Read-only by default, granular write policy engine available.

{
  "mcpServers": {
    "pacsys": {
      "command": "python",
      "args": ["-m", "pacsys.mcp"]
    }
  }
}

Experimental Utilities

pacsys.exp provides high-level utilities for experiment workflows:

from pacsys.exp import Monitor, read_fresh, watch, scan, DataLogger, CsvWriter, ParquetWriter

# Collect readings for 10 seconds
result = Monitor(["M:OUTTMP@p,1000", "G:AMANDA@e,8f"]).collect(duration=10)
print(result.mean("M:OUTTMP@p,1000"))
print(result.median("M:OUTTMP@p,1000"))

# Continuous monitoring with blocking wait
mon = Monitor(["M:OUTTMP@p,1000"])
mon.start()
reading = mon.await_next("M:OUTTMP@p,1000")  # block until next reading
snap = mon.snapshot()                          # all buffered data still available
mon.stop()

# Time-slice and export
sliced = result.slice("M:OUTTMP@p,1000", start=t0, end=t1)
timestamps, values = result.to_numpy("M:OUTTMP@p,1000")
df = result.to_dataframe(relative=True)  # elapsed seconds index

# Wait for fresh readings (with multi-count and stats)
results = read_fresh(["M:OUTTMP@p,1000"], count=10, timeout=5.0)
print(results[0].mean())  # windowed stats: mean, std, median, min, max

# Watch for a condition
reading = watch("M:OUTTMP@p,1000", lambda r: r.value > 75, timeout=30)

# Parameter scan with restore
result = scan("Z:ACLTST", ["M:OUTTMP"], values=[0.0, 1.0, 2.0], settle=0.5)

# Log to CSV
with DataLogger(["M:OUTTMP@p,1000"], writer=CsvWriter("log.csv")):
    time.sleep(60)

# Log to Parquet (typed columns, ZSTD compression)
with DataLogger(["M:OUTTMP@p,1000"], writer=ParquetWriter("log.parquet")):
    time.sleep(60)

CLI Tools

EPICS-style command-line tools:

# Read devices
acget M:OUTTMP Z:ACLTST
acget --format json M:OUTTMP

# Write devices (requires authentication, kerberos attempted by default)
acput Z:ACLTST 72.5
acput -a kerberos -b dmq --verify --tolerance 0.5 Z:ACLTST 72.5

# Monitor (streaming on default event or custom one)
acmonitor M:OUTTMP
acmonitor -n 10 M:OUTTMP@p,500

# Device info (DB + property reads)
acinfo -v M:OUTTMP

Also aliased under pacsys-get, pacsys-put, pacsys-monitor, pacsys-info.

Requirements

  • Python 3.10+
  • For writes: Kerberos credentials with appropriate role or console class assigned
  • For some utilities: must run on the controls network or have SSH access to it

Documentation

See the full documentation for guides, API reference, and protocol details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pacsys-0.2.2.tar.gz (413.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pacsys-0.2.2-py3-none-any.whl (315.2 kB view details)

Uploaded Python 3

File details

Details for the file pacsys-0.2.2.tar.gz.

File metadata

  • Download URL: pacsys-0.2.2.tar.gz
  • Upload date:
  • Size: 413.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for pacsys-0.2.2.tar.gz
Algorithm Hash digest
SHA256 f3624e5baf5caf304bbb0435ed5be44b8ff4ad51cd3f8d1404ef1ee53f13ac62
MD5 d2e5100c209ffc79ca35343147afa4b7
BLAKE2b-256 faec473866b16e1e5c6d49a7ad7d6387f51ccf80ed6274a0eda0957e11e0c715

See more details on using hashes here.

File details

Details for the file pacsys-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: pacsys-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 315.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for pacsys-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cb9f1f0b3200488125dab3ca0cebaa92ca728fe3078731d4206ba916fd28cd01
MD5 c53ad15f4ec46e4432431b13216b9986
BLAKE2b-256 a7f9e54fb053e95c928df3e9c64c0deed2f6a6cc025c922aa638a797ed84f0d9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page