Skip to main content

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
  • Low-level ACNET protocol handlers for GETS32/RETDAT/FTPMAN
  • 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            # Includes Kerberos and SSH dependencies
pip install "pacsys[all]"     # + Parquet and MCP dependencies
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.confirmed

# 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 the same API surface.

import pacsys
import pacsys.aio as aio
from pacsys.aio import AsyncDevice

# 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
    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.SSHClient(["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.SSHClient("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: -a kerberos, or -a jwt with -b grpc)
acput -a kerberos 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.

Metadata

Release files for pacsys 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pacsys 0.3.0
File Size Uploaded Provenance Actions
pacsys-0.3.0.tar.gz 349.9 kB Without provenance Details for pacsys-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pacsys 0.3.0
File Interpreter ABI Platform Actions
pacsys-0.3.0-py3-none-any.whl Python 3 none any Details for pacsys-0.3.0-py3-none-any.whl

Total release size: 739.4 kB

Release files / pacsys-0.3.0.tar.gz

Download URL pacsys-0.3.0.tar.gz
Size 349.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b68a2de4862a66d0d382dfbfd1a9dd4d6a965b4dc85d4a9b1f5617e48b4791f0
BLAKE2b-256 checksum
How to use checksums
1b7bceacba384db620d2a271d5e60f0bae8faa869ea85dacccc2d07538d5fa00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release files / pacsys-0.3.0-py3-none-any.whl

Download URL pacsys-0.3.0-py3-none-any.whl
Size 389.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c2ff694cfae84ea151388fd406f277515135cd45e794848824e3054300d0dcb
BLAKE2b-256 checksum
How to use checksums
024f11bf3ebdd0d6957f2a635cbfa48ad5478081f24debd3052a7836f9783fc7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Security for pacsys 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page