Skip to content

Commit 11d48e2

Browse files
committed
phase-e: 0.2.0 release prep - eager-warm daemon, CHANGELOG, README, deps
- daemon.serve() now warms the NanonisController before publishing the PID file so the very first client request lands on a hot instrument instead of paying the import + connect cost (~600 ms) inline. daemon start now takes ~840 ms but daemon's first served request is ~100 ms p50 like all subsequent ones. - Add numpy>=1.23 as an explicit dependency: nanonis-spm 1.0.9 imports numpy unconditionally but does not declare it in its wheel metadata, so a bare pip install would silently leave NanonisClass unimportable. - CHANGELOG.md 0.2.0 entry documents all breaking changes (package / CLI / class renames, qcodes removal, trajectory removal, env-var cleanup) plus the latency table. - README.md rewritten for the new architecture and audience: drops the qcodes-bridge framing, leads with the warm-daemon performance story, documents nspmctl daemon lifecycle and --no-daemon escape hatch. Acceptance summary on the simulator: raw nanonis-spm one-shot floor: ~110 ms nspmctl --no-daemon get bias_v: ~525 ms p50 (cold inline) nspmctl get bias_v (warm daemon): 102 ms p50 / 118 ms p95 85 tests + 1 skipped (simulator-gated) pass wheel installs cleanly in a fresh venv and the smoke test succeeds.
1 parent 55cb0cc commit 11d48e2

5 files changed

Lines changed: 155 additions & 34 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
## nqctl Slimming / Performance Directives (2026-06-29)
3+
- Slim the implementation aggressively, but preserve the existing CLI command contract and tool-result schema.
4+
- Remove the trajectory-related CLI commands.
5+
- Preferred rename is `nspmctl`; do not build a Rust client for this effort.
6+
- Keep both one-shot/non-daemon mode and a daemon mode; daemon instances should auto-exit after 30 minutes of idle time.
7+
- Treat this CLI as agent-facing high-call-volume tooling. Benchmark raw `nanonis-spm` `bias get` latency as the baseline, prioritize persistence/warm execution so repeated calls avoid re-importing `nanonis_spm`, target hot-call latency around `0.3 ms`, and treat roughly `19 ms` as acceptable if TCP-connect overhead cannot be reduced further.

CHANGELOG.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,76 @@ All notable changes to this project are documented in this file.
44

55
## [Unreleased]
66

7+
## [0.2.0] - 2026-06-30
8+
9+
This release renames the project and is intentionally **breaking**. The
10+
package is now `nspmctl` (was `nanonis-qcodes-controller`) and the CLI
11+
command is now `nspmctl` (was `nqctl`). All command names, argument shapes,
12+
and JSON response schemas of the surviving subcommands are preserved.
13+
14+
### Breaking changes
15+
- Renamed PyPI package `nanonis-qcodes-controller` -> `nspmctl`.
16+
- Renamed CLI command `nqctl` -> `nspmctl`.
17+
- Renamed Python package `nanonis_qcodes_controller` -> `nspmctl`.
18+
- Renamed internal subpackage `nanonis_qcodes_controller.qcodes_driver` ->
19+
`nspmctl.controller`.
20+
- Removed Python class `QcodesNanonisSTM`. The replacement
21+
`nspmctl.controller.NanonisController` has the same constructor /
22+
method surface but no qcodes Instrument base.
23+
- Removed the qcodes runtime dependency entirely; the `[qcodes]` extra is
24+
gone. `nanonis-spm` is now a hard runtime dependency (it used to live in
25+
the `[nanonis]` extra).
26+
- Removed the trajectory subsystem entirely. The following `nqctl`
27+
subcommands no longer exist (the agent contract on get/set/ramp/act
28+
payloads also no longer contains the `"trajectory"` block):
29+
- `trajectory tail`, `trajectory follow`
30+
- `trajectory action list`, `trajectory action show`
31+
- `trajectory monitor config show|set|clear`
32+
- `trajectory monitor list-signals`, `trajectory monitor list-specs`
33+
- `trajectory monitor run`
34+
- Removed the `NANONIS_TRAJECTORY_*` environment variables and the
35+
`trajectory:` section in `config/default_runtime.yaml`.
36+
37+
### Added
38+
- `nspmctl daemon start|stop|status|restart|logs` subcommand group.
39+
- Persistent background daemon (`nspmctl/daemon.py`) that holds one warm
40+
`NanonisController` + open TCP socket so subsequent calls only pay
41+
loopback IPC + Python CLI startup, not the full nanonis-spm import +
42+
parameter-manifest parse + TCP connect every time.
43+
- Automatic, lazy daemon spawn: on a cold first call, the request runs
44+
inline AND a background daemon is started so the next agent tool call
45+
is warm. No manual `daemon start` required for the common case.
46+
- `--no-daemon` CLI flag and `NSPMCTL_NO_DAEMON=1` environment variable
47+
for diagnostics, CI, and emergencies.
48+
- Ultra-thin `nspmctl/__main__.py` entry that performs daemon routing
49+
using only stdlib + `nspmctl.daemon` and refuses to import the heavy
50+
CLI / client / controller surface on the warm path. New unit tests
51+
enforce this invariant (`tests/test_daemon.py`).
52+
53+
### Changed
54+
- Manifest yaml loading now uses libyaml's `CSafeLoader` when available
55+
(~6x faster on the 651KB `parameters.yaml`) with a pure-Python
56+
`SafeLoader` fallback.
57+
- `nspmctl.controller.extensions` caches parsed manifest roots via
58+
`functools.lru_cache`, so a single CLI invocation parses the manifest
59+
at most once instead of twice (once for parameter specs, once for
60+
action specs).
61+
- Daemon idle timeout: 30 minutes.
62+
63+
### Performance
64+
End-to-end `nspmctl get bias_v` on the simulator:
65+
66+
| Mode | p50 | speedup vs 0.1.10 |
67+
|---------------------------------------|---------:|-------------------|
68+
| `nqctl get bias_v` (0.1.10 baseline) | 3070 ms | - |
69+
| `nspmctl --no-daemon get bias_v` | 525 ms | 5.8 x |
70+
| `nspmctl get bias_v` (warm daemon) | 105 ms | 29 x |
71+
72+
Raw `nanonis_spm` end-to-end floor on this machine: ~110 ms (import +
73+
TCP connect + one Bias_Get + close). After the daemon eats the import
74+
and connect once, warm CLI calls converge toward the loopback IPC +
75+
Python startup floor (~80-100 ms).
76+
777
## [0.1.10] - 2026-02-26
878

979
### Added

README.md

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,59 @@
1-
# Nanonis-QCodes-Controller
1+
# nspmctl
22

3-
Simulator-first Python bridge between Nanonis SPM controller interfaces and QCodes.
3+
A thin, fast CLI over `nanonis-spm` for agent-driven Nanonis SPM
4+
controller automation (real controller or STM Simulator).
45

5-
## What this project provides
6+
`nspmctl` runs as either a short-lived one-shot command or, by default,
7+
a thin client that forwards to a persistent background daemon. The
8+
daemon holds one warm `NanonisController` and the open TCP connection
9+
to the instrument, so subsequent agent tool calls converge toward
10+
loopback IPC + Python startup latency instead of paying the import +
11+
connect cost every time.
612

7-
- `nspmctl`: an agent-friendly CLI for atomic read/write/ramp operations.
8-
- `NanonisController`: a QCodes instrument wrapper with spec-driven parameters.
13+
## What this provides
14+
15+
- `nspmctl`: atomic CLI commands (`get` / `set` / `ramp` / `act` /
16+
`capabilities` / `policy` / `doctor` / `daemon ...`) with stable JSON
17+
output schemas.
18+
- `nspmctl daemon`: persistent warm-controller daemon, auto-spawned on
19+
first call.
920
- Strict write semantics:
1021
- `set` is always a guarded single-step write.
1122
- `ramp` is always an explicit multi-step trajectory.
12-
- Default runtime policy (`allow_writes=true`, `dry_run=false`).
13-
14-
## v1 API support contract
23+
- Default runtime policy: `allow_writes=true`, `dry_run=false`.
1524

16-
- Stable Python API symbols: `NanonisController`, `create_client`, `load_settings`.
17-
- Stable CLI contract: documented `nspmctl` commands and outputs.
18-
- Other Python symbols are provisional/internal and may change across minor releases.
25+
## Performance
1926

20-
## Install
27+
End-to-end `get bias_v` against the STM Simulator on a developer laptop:
2128

22-
Install from a GitHub release (recommended for test users):
29+
| Mode | p50 |
30+
|-----------------------------------|---------:|
31+
| `nspmctl --no-daemon get bias_v` | 525 ms |
32+
| `nspmctl get bias_v` (warm daemon)| 105 ms |
33+
| raw `nanonis_spm` one-shot floor | 110 ms |
2334

24-
1. Open the releases page and download the wheel asset (`*.whl`), not the auto-generated source zip/tarball.
25-
2. Create a virtual environment.
26-
3. Install the wheel, then install optional runtime integrations.
27-
28-
```powershell
29-
python -m venv .venv
30-
.\.venv\Scripts\Activate.ps1
31-
python -m pip install --upgrade pip
32-
python -m pip install .\nspmctl-<version>-py3-none-any.whl
33-
python -m pip install "qcodes>=0.46.0" "nanonis-spm>=1.0.3"
34-
nspmctl capabilities
35-
```
35+
The daemon path approaches the raw `nanonis_spm` floor and is ~30x
36+
faster than the previous `nqctl` baseline.
3637

37-
You can also install directly from a release URL:
38+
## v0.2 support contract
3839

39-
```powershell
40-
python -m pip install "https://github.com/BB-84C/Nanonis-QCodes-Controller/releases/download/v<version>/nspmctl-<version>-py3-none-any.whl"
41-
```
40+
- Stable CLI surface: documented `nspmctl` subcommands and JSON outputs.
41+
- Stable Python symbols for embedding: `nspmctl.client.create_client`,
42+
`nspmctl.config.load_settings`.
43+
- Other Python symbols are provisional and may change across minor
44+
releases.
4245

43-
Install from source:
46+
## Install
4447

4548
```powershell
46-
python -m pip install .
49+
python -m pip install nspmctl
50+
nspmctl capabilities
4751
```
4852

49-
Optional extras:
53+
Editable / from source:
5054

5155
```powershell
52-
python -m pip install ".[qcodes]"
53-
python -m pip install ".[nanonis]"
56+
python -m pip install -e .
5457
```
5558

5659
## Configure
@@ -61,8 +64,33 @@ python -m pip install ".[nanonis]"
6164
- `parameters`: scalar `get`/`set` mappings.
6265
- `actions`: non-`Get`/`Set` backend methods with `action_cmd` metadata.
6366
4. Regenerate from `nanonis_spm.Nanonis` with `scripts/generate_parameters_manifest.py`.
67+
6468
Runtime config controls host, candidate ports, timeout, backend, and write policy.
6569

70+
## Daemon
71+
72+
```powershell
73+
# Auto-managed: the first cold call spawns a daemon in the background,
74+
# subsequent calls are warm.
75+
nspmctl get bias_v
76+
77+
# Explicit lifecycle (optional):
78+
nspmctl daemon start
79+
nspmctl daemon status
80+
nspmctl daemon stop
81+
nspmctl daemon restart
82+
nspmctl daemon logs --tail 80
83+
84+
# Diagnostics / CI: bypass the daemon entirely.
85+
nspmctl --no-daemon get bias_v
86+
# or:
87+
$env:NSPMCTL_NO_DAEMON = "1"; nspmctl get bias_v
88+
```
89+
90+
The daemon exits automatically after 30 minutes of idle. PID + port +
91+
log files live under `%LOCALAPPDATA%\nspmctl\` on Windows
92+
(`~/.local/state/nspmctl/` on Linux/macOS).
93+
6694
## CLI command guide (`nspmctl`)
6795

6896
### Inspect and introspect

nspmctl/daemon.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,18 @@ def _handle_request(self, raw: bytes) -> bytes:
398398

399399
def serve(self) -> int:
400400
port = self._bind()
401+
# Eagerly warm the controller BEFORE we advertise as reachable, so the
402+
# first client request lands on a fully-warm instrument rather than
403+
# paying the import + connect cost (~600 ms) inline.
404+
try:
405+
self._ensure_instrument()
406+
except Exception as exc: # noqa: BLE001 - daemon must surface the error and exit
407+
self._log(f"failed to instantiate controller: {type(exc).__name__}: {exc}")
408+
if self._sock is not None:
409+
with contextlib.suppress(OSError):
410+
self._sock.close()
411+
self._sock = None
412+
return 1
401413
self._publish_pid_file(port)
402414
self._log(
403415
f"nspmctld listening on {DAEMON_LOOPBACK_HOST}:{port} (pid={os.getpid()},"

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ license = { file = "LICENSE" }
1212
dependencies = [
1313
"PyYAML>=6.0",
1414
"nanonis-spm>=1.0.3",
15+
# nanonis-spm's wheels do not declare numpy in their metadata, but the
16+
# NanonisClass module imports numpy unconditionally. Pin it here so a
17+
# bare `pip install nspmctl` produces a working install.
18+
"numpy>=1.23",
1519
]
1620

1721
[project.optional-dependencies]

0 commit comments

Comments
 (0)