Skip to content

Commit

Permalink
Merge b0390d5 into e2670f3
Browse files Browse the repository at this point in the history
  • Loading branch information
BrianJKoopman committed Aug 15, 2022
2 parents e2670f3 + b0390d5 commit 40f2cb0
Show file tree
Hide file tree
Showing 20 changed files with 2,449 additions and 761 deletions.
18 changes: 18 additions & 0 deletions agents/barebones_agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# OCS Barebones Agent
# ocs Agent for demonstrating how to write an Agent

# Use ocs base image
FROM ocs:latest

# Set the working directory to registry directory
WORKDIR /app/ocs/agents/barebones_agent/

# Copy this agent into the WORKDIR
COPY . .

# Run registry on container startup
ENTRYPOINT ["dumb-init", "python3", "-u", "barebones_agent.py"]

# Sensible defaults for crossbar server
CMD ["--site-hub=ws://crossbar:8001/ws", \
"--site-http=http://crossbar:8001/call"]
190 changes: 190 additions & 0 deletions agents/barebones_agent/barebones_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import time
import txaio

from os import environ

from ocs import ocs_agent, site_config
from ocs.ocs_twisted import TimeoutLock


class BarebonesAgent:
"""Barebone Agent demonstrating writing an Agent from scratch.
This Agent is meant to be an example for Agent development, and provides a
clean starting point when developing a new Agent.
Parameters:
agent (OCSAgent): OCSAgent object from :func:`ocs.ocs_agent.init_site_agent`.
Attributes:
agent (OCSAgent): OCSAgent object from :func:`ocs.ocs_agent.init_site_agent`.
log (txaio.tx.Logger): Logger object used to log events within the
Agent.
lock (TimeoutLock): TimeoutLock object used to prevent simultaneous
commands being sent to hardware.
_count (bool): Internal tracking of whether the Agent should be
counting or not. This is used to exit the Process loop by changing
it to False via the count.stop() command. Your Agent won't use this
exact attribute, but might have a similar one.
"""

def __init__(self, agent):
self.agent = agent
self.log = agent.log
self.lock = TimeoutLock(default_timeout=5)
self._count = False

# Register OCS feed
agg_params = {
'frame_length': 10 * 60 # [sec]
}
self.agent.register_feed('feed_name',
record=True,
agg_params=agg_params,
buffer_time=1.)

def count(self, session, params):
"""count(test_mode=False)
**Process** - Count up from 0.
The count will restart if the process is stopped and restarted.
Notes:
The most recent value is stored in the session data object in the
format::
>>> response.session['data']
{"value": 0,
"timestamp":1600448753.9288929}
"""
with self.lock.acquire_timeout(timeout=0, job='count') as acquired:
if not acquired:
print("Lock could not be acquired because it " +
f"is held by {self.lock.job}")
return False

session.set_status('running')

# Initialize last release time for lock
last_release = time.time()

# Initialize the counter
self._count=True
counter = 0

self.log.info("Starting the count!")

# Main process loop
while self._count:
# About every second, release and acquire the lock
if time.time() - last_release > 1.:
last_release = time.time()
if not self.lock.release_and_acquire(timeout=10):
print(f"Could not re-acquire lock now held by {self.lock.job}.")
return False

# Perform the process actions
counter += 1
self.log.debug(f"{counter}! Ah! Ah! Ah!")
now = time.time()
session.data = {"value": counter,
"timestamp": now}

# Format message for publishing to Feed
message = {'block_name': 'count',
'timestamp': now,
'data': {'value': counter}}
self.agent.publish_to_feed('feed_name', message)
time.sleep(1)

self.agent.feeds['feed_name'].flush_buffer()

return True, 'Acquisition exited cleanly.'

def _stop_count(self, session, params):
"""Stop monitoring the turbo output."""
if self._count:
self._count = False
return True, 'requested to stop taking data.'
else:
return False, 'count is not currently running'

@ocs_agent.param('text', default='hello world', type=str)
def print(self, session, params):
"""print(text='hello world')
**Task** - Print some text passed to a Task.
Args:
text (str): Text to print out. Defaults to 'hello world'.
Notes:
The session data will be updated with the text::
>>> response.session['data']
{'text': 'hello world',
'last_updated': 1660249321.8729222}
"""
with self.lock.acquire_timeout(timeout=3.0, job='print') as acquired:
if not acquired:
self.log.warn("Lock could not be acquired because it " +
f"is held by {self.lock.job}")
return False

# Set operations status to 'running'
session.set_status('running')

# Log the text provided to the Agent logs
self.log.info(f"{params['text']}")

# Store the text provided in session.data
session.data = {'text': params['text'],
'last_updated': time.time()}

# bool, 'descriptive text message'
# True if task succeeds, False if not
return True, 'Printed text to logs'


def add_agent_args(parser_in=None):
if parser_in is None:
from argparse import ArgumentParser as A
parser_in = A()
pgroup = parser_in.add_argument_group('Agent Options')
pgroup.add_argument('--mode', type=str, default='count',
choices=['idle', 'count'],
help="Starting action for the Agent.")

return parser_in


if __name__ == '__main__':
# For logging
txaio.use_twisted()
LOG = txaio.make_logger()

# Start logging
txaio.start_logging(level=environ.get("LOGLEVEL", "info"))

parser = add_agent_args()
args = site_config.parse_args(agent_class='BarebonesAgent', parser=parser)

startup = False
if args.mode == 'count':
startup = True

agent, runner = ocs_agent.init_site_agent(args)

barebone = BarebonesAgent(agent)
agent.register_process(
'count',
barebone.count,
barebone._stop_count,
startup=startup)
agent.register_task('print', barebone.print)

runner.run(agent, auto_reconnect=True)
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ services:
build: ./agents/fake_data/
depends_on:
- "ocs"

# --------------------------------------------------------------------------
# The OCS Barebones Agent
# --------------------------------------------------------------------------
ocs-barebones-agent:
image: "ocs-barebones-agent"
build: ./agents/barebones_agent/
depends_on:
- "ocs"
31 changes: 31 additions & 0 deletions docs/agents/barebones.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.. _barebones:

===============
Barebones Agent
===============

The Barebones Agent is provided with OCS to provide a starting point for Agent
development. It is heavily used throughout the Agent development guide.

Configuration File Examples
---------------------------

Below are configuration examples for the ocs config file and for running the
Agent in a docker container.

OCS Site Config
```````````````

To configure the Fake Data Agent we need to add a FakeDataAgent block to our
ocs configuration file. Here is an example configuration block using all of the
available arguments::

{'agent-class': 'BarebonesAgent',
'instance-id': 'barebones-1',
'arguments': []},

Agent API
---------

.. autoclass:: agents.barebones_agent.barebones_agent.BarebonesAgent
:members:
143 changes: 143 additions & 0 deletions docs/developer/agent_references/documentation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
.. _documentation:

Documentation
-------------

Documentation is important for users writing OCSClients that can interact with
your new Agent. When writing a new Agent you must document the Tasks and
Processes with appropriate docstrings. Additionally a page must be created
within the docs to describe the Agent and provide other key information such as
configuration file examples. You should aim to be a thorough as possible when
writing documentation for your Agent.

Task and Process Documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Each Task and Process within an Agent must be accompanied by a docstring. Here
is a complete example of a well documented Task (or Process)::

@ocs_agent.param('arg1', type=bool)
@ocs_agent.param('arg2', default=7, type=int)
def demo(self, session, params):
"""demo(arg1=None, arg2=7)

**Task** (or **Process**) - An example task docstring for illustration purposes.

Parameters:
arg1 (bool): Useful argument 1.
arg2 (int, optional): Useful argument 2, defaults to 7. For details see
:func:`socs.agent.demo_agent.DemoClass.detailing_method`

Examples:
Example for calling in a client::

client.demo(arg1=False, arg2=5)

Notes:
An example of the session data::

>>> response.session['data']
{"fields":
{"Channel_05": {"T": 293.644, "R": 33.752, "timestamp": 1601924482.722671},
"Channel_06": {"T": 0, "R": 1022.44, "timestamp": 1601924499.5258765},
"Channel_08": {"T": 0, "R": 1026.98, "timestamp": 1601924494.8172355},
"Channel_01": {"T": 293.41, "R": 108.093, "timestamp": 1601924450.9315426},
"Channel_02": {"T": 293.701, "R": 30.7398, "timestamp": 1601924466.6130798}
}
}
"""
pass

Keep reading for more details on what's going on in this example.

Overriding the Method Signature
```````````````````````````````
``session`` and ``params`` are both required parameters when writing an OCS
Task or Process, but both should be hidden from users writing OCSClients. When
documenting a Task or Process, the method signature should be overridden to
remove both ``session`` and ``params``, and to include any parameters your Task
or Process might take. This is done in the first line of the docstring, by
writing the method name, followed by the parameters in parentheses. In the
above example that looks like::

def demo(self, session, params=None):
"""demo(arg1=None, arg2=7)"""

This will render the method description as ``delay_task(arg1=None,
arg2=7)`` within Sphinx, rather than ``delay_task(session, params=None)``. The
default values should be put in this documentation. If a parameter is required,
set the param to ``None`` in the method signature. For more info on the
``@ocs_agent.param`` decorator see :ref:`param`.

Keyword Arguments
`````````````````
Internal to OCS the keyword arguments provided to an OCSClient are passed as a
`dict` to ``params``. For the benefit of the end user, these keyword arguments
should be documented in the Agent as if passed as such. So the docstring should
look like::

Parameters:
arg1 (bool): Useful argument 1.
arg2 (int, optional): Useful argument 2, defaults to 7. For details see
:func:`socs.agent.lakeshore.LakeshoreClass.the_method`

Examples
````````
Examples should be given using the "Examples" heading when it would improve the
clarity of how to interact with a given Task or Process::

Examples:
Example for calling in a client::

client.demo(arg1=False, arg2=5)

Session Data
````````````
The ``session.data`` object structure is left up to the Agent author. As such,
it needs to be documented so that OCSClient authors know what to expect. If
your Task or Process makes use of ``session.data``, provide an example of the
structure under the "Notes" heading. On the OCSClient end, this
``session.data`` object is returned in the response under
``response.session['data']``. This is how it should be presented in the example
docstrings::

Notes:
An example of the session data::

>>> response.session['data']
{"fields":
{"Channel_05": {"T": 293.644, "R": 33.752, "timestamp": 1601924482.722671},
"Channel_06": {"T": 0, "R": 1022.44, "timestamp": 1601924499.5258765},
"Channel_08": {"T": 0, "R": 1026.98, "timestamp": 1601924494.8172355},
"Channel_01": {"T": 293.41, "R": 108.093, "timestamp": 1601924450.9315426},
"Channel_02": {"T": 293.701, "R": 30.7398, "timestamp": 1601924466.6130798}
}
}

For more details on the ``session.data`` object see :ref:`session_data`.

Agent Reference Pages
^^^^^^^^^^^^^^^^^^^^^
Now that you have documented your Agent's Tasks and Processes appropriately we
need to make the page that will display that documentation. Agent reference
pages are kept in `ocs/docs/agents/
<https://github.com/simonsobs/ocs/tree/develop/docs/agents>`_. Each Agent has a
separate `.rst` file. Each Agent reference page must contain:

* Brief description of the Agent
* Example ocs-site-config configuration block
* Example docker-compose configuration block (if Agent is dockerized)
* Agent API reference

Reference pages can also include:

* Detailed description of Agent or related material
* Example client scripts
* Supporting APIs

Here is a template for an Agent documentation page. Text starting with a '#' is
there to guide you in writing the page and should be replaced or removed.
Unneeded sections should be removed.

.. include:: ../../example/docs/agent_template.rst
:code: rst

0 comments on commit 40f2cb0

Please sign in to comment.