# cerillo

Cerillo python API for control of Cerillo devices

[![PyPI - Version](https://img.shields.io/pypi/v/cerillo.svg)](https://pypi.org/project/cerillo)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cerillo.svg)](https://pypi.org/project/cerillo)

-----

## Table of Contents

- [cerillo](#cerillo)
  - [Table of Contents](#table-of-contents)
  - [Installation](#installation)
  - [Usage](#usage)
    - [StratusReader](#stratusreader)
    - [RayoReader](#rayoreader)
    - [Advanced experiment setup (CDCL ExperimentBuilder)](#advanced-experiment-setup-cdcl-experimentbuilder)
  - [License](#license)
  - [ExperimentResults (exporting \& inspecting data)](#experimentresults-exporting--inspecting-data)

## Installation

`pip install cerillo`

## Usage

Below are concrete examples showing how to instantiate, connect, control, and disconnect `StratusReader` and `RayoReader`. These examples use the real public APIs implemented in `cerillo.base_plate_reader` and the reader modules.

### StratusReader

The `StratusReader` extends `CerilloBasePlateReader`. Key methods on the base class include `connect()`, `disconnect()`, and the newer experiment APIs: `start_kinetic_experiment(...)`, `start_endpoint_experiment(...)`, and the higher-level `start_experiment(...)` that accepts a `FullExperimentBuilder`.

Note: `read_absorbance(...)` is deprecated. Use `start_kinetic_experiment()` for time-series (kinetic) experiments or `start_endpoint_experiment()` for single endpoint measurements.

Example (kinetic experiment, simulation):

```python
from cerillo.stratus import StratusReader

# Create a reader. Use simulate=True for local testing without hardware.
reader = StratusReader(port="/dev/ttyUSB0", simulate=True)
reader.connect()
print(reader)

# Start a kinetic experiment: wavelength (nm), interval (seconds), optional duration (seconds)
results = reader.start_kinetic_experiment(wavelength=600, interval=60, duration=3600)

# `results` is an ExperimentResults instance - see ExperimentResults section below
print(results.to_json())

reader.disconnect()
```

Example (endpoint experiment):

```python
from cerillo.stratus import StratusReader

reader = StratusReader(simulate=True)
reader.connect()
results = reader.start_endpoint_experiment(wavelength=600)
print(results.to_csv(format='long'))
reader.disconnect()
```

### RayoReader

`RayoReader` also extends `CerilloBasePlateReader` and adds motor control utilities. The constructor accepts `has_motor` to enable motor operations and `simulate` for dry runs.


Example (motor commands + experiments in simulation):

```python
from cerillo.rayo import RayoReader, MotorStepCommand, MotorNamedCommand

# Create a Rayo reader. Enable simulate for dry-run and has_motor if device has a motor.
reader = RayoReader(port="/dev/ttyUSB0", has_motor=True, simulate=True)

# Connect (simulation will populate device info)
reader.connect()
print(reader)

# Start a kinetic experiment (time-series). Returns ExperimentResults
kinetic_results = reader.start_kinetic_experiment(wavelength=590, interval=30, duration=1800)
print("Kinetic JSON:\n", kinetic_results.to_json())

# Start an endpoint experiment (single measurement)
endpoint_results = reader.start_endpoint_experiment(wavelength=590)
print("Endpoint CSV (long):\n", endpoint_results.to_csv(format='long'))

# Move motor by a number of steps (positive/negative). If simulate=True this will print simulated responses.
step_cmd = MotorStepCommand(steps=100)
ok = reader.move_motor(step_cmd)
print("Motor step result:", ok)

# Use named commands for common operations: 'c' (close) and 'o' (open) are provided helpers.
close_ok = reader.close_lid()
open_ok = reader.open_lid()
print("Close lid result:", close_ok, "Open lid result:", open_ok)

# When done
reader.disconnect()
```

Notes:
- If `move_motor` is called on a reader constructed without `has_motor=True`, it will return False and print a helpful message.
- Motor commands are represented by `MotorStepCommand` (step count) and `MotorNamedCommand` (string commands). Both inherit the CDCL command base and are sent with `send_command` internally. `RayoReader::open_lid()` and `RayoReader::close_lid()` use the `MotorNamedCommand`.

Kinetic vs Endpoint summary:
- Kinetic experiments collect repeated timepoints and are started with `start_kinetic_experiment(wavelength, interval, duration)`.
- Endpoint experiments take single or sparse measurements and are started with `start_endpoint_experiment(wavelength)`.

Both return an `ExperimentResults` object (or `start_experiment` will create/populate one) — see the `ExperimentResults` section below for how to work with and export data.


### Advanced experiment setup (CDCL ExperimentBuilder)

A minimal flow to create an experiment and start it with the reader:

```python
from cerillo.cdcl.experiment_builder import ExperimentBuilder
from cerillo.stratus import StratusReader

# Build an experiment configuration
builder = FullExperimentBuilder()
builder.experiment().set_name("example_experiment").set_interval(60).set_duration(3600)
builder.plate().set_manufacturer("corning")
# add plates, templates, etc. via builder API
# builder.set_interval(...)


# Create reader and start the experiment
reader = StratusReader(simulate=True)
reader.connect()
try:
    # Use start_experiment if you want the reader to send the builder messages and collect data
    data = reader.start_experiment(builder)
    print("Experiment data:", data)
finally:
    reader.disconnect()
```

## License

`cerillo` is distributed under the terms of the [GPLv3](https://spdx.org/licenses/GPL-3.0-or-later.html) license.

## ExperimentResults (exporting & inspecting data)

When you start an experiment with `start_experiment`, `start_kinetic_experiment`, or `start_endpoint_experiment` the reader will return an `ExperimentResults` instance. This container provides utilities to inspect and export collected data.

Common methods and examples:

- `to_json(indent=2)` : return a JSON string of the full results.
- `to_csv(format='long'|'wide', include_metadata=True)` : return CSV in either long (one row per measurement) or wide (one row per timepoint with one column per well) formats.
- `save_json(filepath)` / `save_csv(filepath, format='long')` : convenience methods to write files.
- `get_wells()` : list of wells with data (sorted like `A1, A2, ...`).
- `get_wavelengths()` : list of measured wavelengths.
- `get_data_points(well=None, wavelength=None)` : filter points by well and/or wavelength.
- `get_well_timeseries(well, wavelength=None)` : returns a list of `(timestamp, value)` tuples for the well.

Example: basic exports

```python
# assume `results` is an ExperimentResults instance returned from start_...()
print(results.to_json(indent=2))
csv_long = results.to_csv(format='long')
csv_wide = results.to_csv(format='wide')

# Save to disk
results.save_json('experiment.json')
results.save_csv('experiment_long.csv', format='long')
results.save_csv('experiment_wide.csv', format='wide')
```

Example: export one CSV per well

```python
for well in results.get_wells():
  timeseries = results.get_well_timeseries(well)
  # write a small per-well CSV
  with open(f"results_{well}.csv", 'w') as f:
    f.write('timestamp,datetime,value\n')
    for ts, val in timeseries:
      from datetime import datetime
      f.write(f"{ts},{datetime.fromtimestamp(ts).isoformat()},{val}\n")
```

Example: filter by wavelength or well

```python
# all measurements for well A1
a1_points = results.get_data_points(well='A1')

# all measurements at 600 nm
led600 = results.get_data_points(wavelength=600)
```

These methods make it straightforward to integrate Cerillo experiment output into downstream analysis pipelines or to produce per-plate/per-well exports for collaborators.

