Metadata-Version: 2.4
Name: AEcroscopyWave
Version: 0.1.18
Summary: AEcroscopyWave is a cross-platform, modular Python package within the ecosystem of AEcroscopy (Automated Experiments in Microscopy). It provides an API to generate and prepare experiment waveforms to enable reliable microscopy automation workflows.
Author-email: Yongtao Liu <liuy3@ornl.gov>, Rama Vasudevan <vasudevanrk@ornl.gov>, Md Hasan Jawad Chowdhury <chowdhurym1@ornl.gov>
License: MIT
Keywords: waveform,signal-generation,AFM,National Instruments,lab-control,AI Agent,MCP
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: numpy>=2.3.4
Requires-Dist: matplotlib>=3.10.8
Requires-Dist: scipy>=1.17.0
Requires-Dist: scikit-image>=0.25.0
Requires-Dist: opencv-python-headless>=4.12.0
Requires-Dist: tqdm>=4.67.3
Requires-Dist: h5py>=3.15.1
Requires-Dist: tifffile>=2025.10.16
Requires-Dist: igor2>=0.5.12
Requires-Dist: SciFiReaders>=0.12.4
Requires-Dist: pyNSID>=0.0.7.2
Requires-Dist: fastmcp
Requires-Dist: pywin32>=308; platform_system == "Windows"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: twine; extra == "dev"
Provides-Extra: gui
Requires-Dist: PyQt5; extra == "gui"
Provides-Extra: llm
Requires-Dist: openai; extra == "llm"
Provides-Extra: app
Requires-Dist: streamlit>=1.35; extra == "app"
Requires-Dist: streamlit-autorefresh>=1.0; extra == "app"
Provides-Extra: server
Requires-Dist: fastapi>=0.111; extra == "server"
Requires-Dist: python-multipart>=0.0.9; extra == "server"
Requires-Dist: uvicorn[standard]>=0.29; extra == "server"
Provides-Extra: agent
Requires-Dist: httpx>=0.27; extra == "agent"
Provides-Extra: cloud
Requires-Dist: fastapi>=0.111; extra == "cloud"
Requires-Dist: python-multipart>=0.0.9; extra == "cloud"
Requires-Dist: uvicorn[standard]>=0.29; extra == "cloud"
Requires-Dist: httpx>=0.27; extra == "cloud"
Requires-Dist: streamlit>=1.35; extra == "cloud"

# AEcroscopyWave

AEcroscopyWave is a distributed autonomous microscopy platform for coordinating microscope-side agents, cloud services, experiment planning tools, human approval gates, and data publication.

The package includes interfaces and examples for specific instruments and workflows, but the platform is not tied to a single AFM model. New AFM backends, workflow tools, and result-publishing paths can be added without changing the core distributed architecture.

---

## Deployment Architecture

The system can run across multiple machines connected over a network. A common deployment has a persistent cloud or lab server, one or more instrument PCs, and optional compute workers for analysis or simulation.

### Cloud VM (Linux)

The cloud VM is the persistent hub. All three services are managed by `tmux` and started with a single script:

```bash
cd /data/wavecraft
bash start_servers.sh
```

| Service | Port | Description |
|---|---|---|
| `afw-server` | 8765 | FastAPI orchestrator — instrument registry, job queue (SQLite) |
| Streamlit UI | 8501 | Web dashboard — planner, approvals, queue, results, library |
| Tiled | 8000 | Array data catalog — stores numpy arrays from every job |
| Ollama | 11434 | Local LLM inference for the experiment planner |

Configuration lives in `~/.aecroscopy/config.env` on the cloud VM. Required keys:

```
AFW_AGENT_SECRET=<hex32>           # shared secret — generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
AFW_TILED_API_KEY=<hex32>          # Tiled API key — same command
AFW_CLOUD_SERVER_URL=http://localhost:8765
AFW_TILED_URL=http://localhost:8000
AFW_OLLAMA_URL=http://localhost:11434
```

### Instrument PC

The instrument PC runs the hardware agent for a specific microscope backend. It has no database or UI of its own.

```powershell
# PowerShell — set config
$configDir = Join-Path $env:USERPROFILE ".aecroscopy"
$configFile = Join-Path $configDir "config.env"
New-Item -ItemType Directory -Force $configDir | Out-Null

Add-Content $configFile "AFW_CLOUD_SERVER_URL=http://localhost:8765"
Add-Content $configFile "AFW_AGENT_SECRET=<same hex32>"
Add-Content $configFile "AFW_INSTRUMENT_ID=cypher-1"
Add-Content $configFile "AFW_CYPHER_EXECUTABLE=C:\AsylumResearch\v21\Igor Pro Folder\Igor.exe"
Add-Content $configFile "AFW_VI_URL=C:\Users\Asylum User\Desktop\PyScanner_FPGA_6124_01\PyScanner_FPGA_6124_01.exe"
Add-Content $configFile "AFW_TILED_URL=http://localhost:8000"

# Start the agent (keep this terminal open)
afw-agent
```

`AFW_TILED_URL` is used by the agent when it publishes scan arrays. Keep
`http://localhost:8000` only when the SSH tunnel below is running; otherwise set
it to the cloud VM's reachable Tiled URL, for example `http://<vm-ip>:8000`.

### SSH tunnels

The instrument PC reaches all cloud services through a single SSH tunnel. Run this once from the instrument PC (PowerShell):

```powershell
ssh -N `
  -L 8765:localhost:8765 `
  -L 8501:localhost:8501 `
  -L 8000:localhost:8000 `
  -L 11434:localhost:11434 `
  cloud@<vm-ip>
```

After the tunnel is up: the Streamlit UI is at `http://localhost:8501`, Tiled at `http://localhost:8000`, and the agent talks to the orchestrator at `http://localhost:8765`.

Quick connection check from the instrument PC:

```powershell
Invoke-RestMethod http://localhost:8765/health
afw-agent --check
```

## Repository layout

```
wavecraft/
├── aecroscopywave/                    Python package — instrument + cloud side
│   ├── afm_mcp_server.py              `afm-mcp-server` entry point (FastMCP)
│   ├── agent/
│   │   ├── runner.py                  AgentRunner: heartbeat loop, job executor, Tiled publisher
│   │   ├── executor.py                Dispatches job_type → hardware handler (+ named workflow + planner script fallbacks)
│   │   ├── client.py                  OrchestratorClient: register, heartbeat, post_result
│   │   └── cli.py                     `afw-agent` entry point
│   ├── server/
│   │   ├── app.py                     FastAPI cloud orchestrator (afw-server)
│   │   ├── instrument_registry.py     SQLite: instrument table + job queue
│   │   └── cli.py                     `afw-server` entry point
│   ├── app/                           Streamlit dashboard
│   │   ├── main.py                    App entry point + sidebar
│   │   ├── config.py                  AppConfig: reads ~/.aecroscopy/config.env
│   │   ├── state.py                   Shared dashboard session state
│   │   ├── scheduler_daemon.py        Background scheduler launcher
│   │   └── _views/
│   │       ├── planner.py             LLM codegen planner
│   │       ├── approval.py            Human review + enqueue-to-instrument
│   │       ├── queue.py               Live job status (polls afw-server REST API)
│   │       ├── results.py             Completed jobs + Tiled array viewer
│   │       ├── library.py             Named workflow browser
│   │       └── waveform_designer.py   Interactive waveform builder
│   ├── interfaces/                    Hardware drivers
│   │   ├── Cypher.py                  Asylum Research Cypher (Igor Pro COM / TCP)
│   │   ├── Nanosurf.py
│   │   └── WaveVI.py                  NI 6124 DAQ via LabVIEW HTTP server
│   ├── mcp/                           FastMCP server (afm-mcp-server)
│   │   ├── tool_registry.py           Central tool name → callable registry (single source of truth)
│   │   ├── state.py                   Shared MCP state — LRU-bounded waveform store (default 64 entries)
│   │   ├── tools/                     core, wavegen, wavevi, cypher, image, utils, workflow, waveform_registry_tools
│   │   └── workflows/data/            Named multi-step workflow JSON definitions
│   ├── knowledge/                     Distributed run ledger (provenance + knowledge search)
│   │   ├── ledger.py                  Persistent SQLite run ledger
│   │   └── server.py                  HTTP knowledge-ledger service
│   ├── tiled_service/
│   │   ├── publisher.py               TiledPublisher: writes job containers + arrays
│   │   └── collector.py               FolderDataCollector: finds IBW files written by scan
│   ├── validation/                    ApprovalStore (incl. script-hash approval) + digital-twin pre-validation
│   ├── planner/                       LLM codegen planner (Ollama / OpenAI)
│   ├── scheduling/                    Legacy local scheduler (SQLite job queue)
│   └── wavebuilding/                  Waveform primitives
├── digitalAFMTwin/                    Digital AFM twin backends
├── start_servers.sh                   Start all cloud VM services via tmux
└── reports/                           Design notes and gap analyses
```

## Installation

### Cloud VM

```bash
# Full install (server + app + LLM planner)
pip install -e ".[cloud,llm]"

# Include pytest when validating the server before restart
pip install -e ".[cloud,llm,dev]"

# Start all services
bash start_servers.sh
```

### Instrument PC (Windows)

```powershell
# In a short path to avoid Windows long-path issues
python -m venv C:\venvs\wavecraft
C:\venvs\wavecraft\Scripts\activate

# Agent-only install
pip install -e ".[agent]"

# Enable Windows long paths first if needed:
# reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
```

### Requirements

- Python ≥ 3.12
- Cloud VM: Linux, `tmux`, Tiled (`pip install 'tiled[server]'`), Ollama, Ferrosim (`pip install -e git+https://github.com/ramav87/FerroSim.git@master#egg=FerroSim`)
- Instrument PC: backend-specific microscope dependencies
- Optional compute workers: analysis or simulation dependencies required by custom workflows

---

## How jobs flow through the system

```
User opens Streamlit (localhost:8501 via SSH tunnel)
  │
  ├─ Planner page: describe experiment in plain English
  │    Streamlit → Ollama → Python code → Approvals queue
  │
  ├─ Approvals page: review + select instrument + click "Approve & Send"
  │    POST /api/v1/jobs/enqueue → afw-server (SQLite: status=queued)
  │
  ├─ afw-agent heartbeat (every 30 s, via SSH tunnel :8765)
  │    GET /api/v1/agents/heartbeat → server returns next queued job
  │    (server only dispatches when agent reports no active job)
  │
  ├─ afw-agent executes job
  │    job_type=pfm_image   → selected AFM backend
  │    job_type=dc_pulse    → WaveVI HTTP → NI 6124 AO
  │    job_type=reset_scan  → selected AFM backend
  │    job_type=<workflow>  → named workflow from MCP registry
  │    job_type=<any>       → planner script (payload.script_source)
  │
  ├─ Results posted: POST /api/v1/jobs/{id}/result → status=done
  │
  └─ Arrays published: afw-agent → Tiled (via SSH tunnel :8000)
       tiled["runs"][job_id] → pfm_amplitude, pfm_phase arrays
```

---

## Executor fallbacks

The instrument agent handles any job type, not just the built-in hardware commands. When the agent receives a job whose `job_type` is not a registered handler, it falls back in order:

1. **Named workflow** — if `job_type` matches a workflow in the MCP workflow registry (any workflow listed on the Library page), it runs that workflow step by step.
2. **Planner script** — if `payload.script_source` is present, the executor first verifies the script's SHA-256 hash against the `approved_script_hashes` table in the approvals database. If the hash is not found, `ScriptNotApprovedError` is raised and the job is rejected before any code runs. The Streamlit Approvals page records the hash automatically when a reviewer clicks "Approve & Send", so scripts that pass human review are cleared for execution without a second prompt.
3. **Error** — `NotImplementedError` is posted back and the job is marked `error`.

This means any workflow designed on the Library page or generated by the Experiment Planner can be dispatched to the real instrument without any code changes to the agent. LLM-generated scripts always pass through the approval-by-hash gate, ensuring no unsandboxed `exec()` occurs without prior human review.

## Streamlit dashboard

The five dashboard pages all communicate with `afw-server` via the REST API — they do not read any local database.

| Page | Function |
|---|---|
| **Planner** | Describe an experiment in plain English; Ollama generates Python code using MCP tools; send for approval |
| **Approvals** | Review pending proposals; select instrument; click "Approve & Send to Instrument" → enqueues job |
| **Job Queue** | Live view of queued / running / recent jobs polled from afw-server |
| **Results** | Completed jobs with result payloads; "View arrays in Tiled" link (includes API key) |
| **Library** | Browse and inspect named multi-step workflows; trigger from the Planner |

---

## MCP server

```bash
afm-mcp-server
```

Exposes hardware tools to any MCP-compatible LLM agent: waveform generation, backend-specific AFM control, image conversion, and named workflow execution. Built with FastMCP.

### Tool registry

All tool name → callable mappings are stored in a single central registry (`mcp/tool_registry.py`). The FastMCP server, the executor fallback path, and the digital-twin validator all resolve tools through `get_tool(name)`.

For tests or notebooks that need tool resolution without starting the full MCP server, call `populate_tool_registry()`:

```python
from aecroscopywave.mcp.server import populate_tool_registry
populate_tool_registry()        # registers all tools; no FastMCP instance needed

from aecroscopywave.mcp.tool_registry import get_tool
fn = get_tool("cypher_set_master_panel")
```

`get_tool()` also triggers lazy population on first call, so in most cases you can skip the explicit `populate_tool_registry()` import.

### Waveform store

The MCP state module (`mcp/state.py`) maintains an in-memory waveform store for AO waveform references. The store is LRU-bounded with a default cap of 64 entries — once the cap is reached, the least-recently-used entry is evicted. Access via `resolve_ao_waveform_reference()` or `get_ao_waveform_record()` marks an entry as recently used.

## Configuration reference

All values read from environment variables or `~/.aecroscopy/config.env` (env takes precedence).

| Variable | Where | Description |
|---|---|---|
| `AFW_MODE` | both | `server` / `agent` / `standalone` |
| `AFW_AGENT_SECRET` | both | Shared bearer token (hex32) |
| `AFW_CLOUD_SERVER_URL` | both | URL of afw-server, e.g. `http://localhost:8765` |
| `AFW_INSTRUMENT_ID` | agent | Identifier registered with the server |
| `AFW_VI_URL` | agent | WaveVI/PyScanner LabVIEW executable path; legacy `vi_command` jobs may use an HTTP endpoint |
| `AFW_CYPHER_EXECUTABLE` | agent | Path to Igor.exe or COM class |
| `AFW_TILED_URL` | both | Tiled catalog URL |
| `AFW_TILED_API_KEY` | both | Tiled API key (hex32) |
| `AFW_ARTIFACTS_DIR` | server | Server-side landing directory for uploaded instrument artifacts |
| `AFW_OLLAMA_URL` | server | Ollama address |
| `AFW_OLLAMA_MODEL` | server | Model name (empty = auto-detect) |
| `AFW_INSTRUMENT_REGISTRY_DB` | server | Path to instruments+jobs SQLite |
| `AFW_APPROVALS_DB` | both | Path to approvals SQLite (default `~/.aecroscopy/approvals.db`) — stores human-review decisions and script-hash approvals |

---

## Authors

- Rama Vasudevan (vasudevanrk@ornl.gov) — Oak Ridge National Laboratory
- Yongtao Liu (liuy3@ornl.gov) — Oak Ridge National Laboratory
- Md Hasan Jawad Chowdhury (chowdhurym1@ornl.gov) — Oak Ridge National Laboratory

## License

MIT
