Skip to content

Commit 1e36d36

Browse files
committed
Migrate parameters to methods-only command schema
1 parent 76fc3bd commit 1e36d36

File tree

11 files changed

+122
-2956
lines changed

11 files changed

+122
-2956
lines changed

config/parameters.yaml

Lines changed: 1 addition & 1314 deletions
Large diffs are not rendered by default.

docs/cli_contract.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ Expose a small, stable command surface that orchestration agents can call direct
55

66
## Core commands
77
- `nqctl capabilities`: returns parameters/action-commands/actions/policy summary,
8-
including rich `parameters.items[*]` metadata (`get_cmd`, `set_cmd`, `vals`,
8+
including rich `parameters.items[*]` metadata (`get_cmd`, `set_cmd`,
99
`safety`) and `action_commands.items[*]` metadata (`action_cmd`, `safety_mode`).
1010
Descriptions are exposed on command blocks when present.
1111
- `nqctl observables list`: returns readable and writable parameter metadata.
1212
- `nqctl actions list`: returns supported action descriptors.
1313
- `nqctl act <action_name> --arg <key=value>`: invoke one manifest action command.
1414
- `nqctl get <parameter>`: reads one parameter value.
15-
- `nqctl set <parameter> <value>`: guarded strict single-step write.
15+
- `nqctl set <parameter> --arg <key=value>`: guarded strict write using structured argument fields.
1616
- `nqctl ramp <parameter> <start> <end> <step> --interval-s <sec>`: guarded explicit ramp.
1717
- `nqctl policy show`: returns effective write policy and enablement guidance.
1818

nanonis_qcodes_controller/cli.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,6 @@ def _cmd_get(args: argparse.Namespace) -> int:
619619
args, auto_connect=True, include_parameters=(parameter_name,)
620620
) as instrument_ctx:
621621
instrument, journal = instrument_ctx
622-
spec = instrument.parameter_spec(parameter_name)
623622
snapshot = instrument.get_parameter_snapshot(parameter_name)
624623
values = snapshot.get("values", {})
625624
if len(values) == 1:
@@ -630,7 +629,6 @@ def _cmd_get(args: argparse.Namespace) -> int:
630629
"parameter": parameter_name,
631630
"value": _json_safe(value),
632631
"fields": _json_safe(values),
633-
"unit": spec.unit,
634632
"timestamp_utc": _now_utc_iso(),
635633
}
636634
if journal is not None:
@@ -842,7 +840,8 @@ def _cmd_parameters_validate(args: argparse.Namespace) -> int:
842840
"name": spec.name,
843841
"readable": spec.readable,
844842
"writable": spec.writable,
845-
"value_type": spec.value_type,
843+
"has_get_cmd": spec.get_cmd is not None,
844+
"has_set_cmd": spec.set_cmd is not None,
846845
}
847846
for spec in specs
848847
],
@@ -1033,8 +1032,11 @@ def _cmd_trajectory_monitor_list_signals(args: argparse.Namespace) -> int:
10331032
{
10341033
"name": spec.name,
10351034
"label": spec.label,
1036-
"unit": spec.unit,
1037-
"value_type": spec.value_type,
1035+
"response_fields": (
1036+
[]
1037+
if spec.get_cmd is None
1038+
else [asdict(field) for field in spec.get_cmd.response_fields]
1039+
),
10381040
}
10391041
for spec in specs
10401042
if spec.readable
@@ -1051,9 +1053,23 @@ def _cmd_trajectory_monitor_list_specs(args: argparse.Namespace) -> int:
10511053
{
10521054
"name": spec.name,
10531055
"label": spec.label,
1054-
"unit": spec.unit,
1055-
"value_type": spec.value_type,
1056-
"vals": None if spec.vals is None else asdict(spec.vals),
1056+
"get_cmd": (
1057+
None
1058+
if spec.get_cmd is None
1059+
else {
1060+
"command": spec.get_cmd.command,
1061+
"arg_fields": [asdict(field) for field in spec.get_cmd.arg_fields],
1062+
"response_fields": [asdict(field) for field in spec.get_cmd.response_fields],
1063+
}
1064+
),
1065+
"set_cmd": (
1066+
None
1067+
if spec.set_cmd is None
1068+
else {
1069+
"command": spec.set_cmd.command,
1070+
"arg_fields": [asdict(field) for field in spec.set_cmd.arg_fields],
1071+
}
1072+
),
10571073
}
10581074
for spec in specs
10591075
if spec.readable
@@ -1274,10 +1290,8 @@ def _collect_observables(instrument: Any) -> list[dict[str, Any]]:
12741290
{
12751291
"name": spec.name,
12761292
"label": spec.label,
1277-
"unit": spec.unit,
12781293
"readable": spec.readable,
12791294
"writable": spec.writable,
1280-
"value_type": spec.value_type,
12811295
"has_ramp": bool(spec.safety is not None and spec.safety.ramp_enabled),
12821296
}
12831297
)
@@ -1318,15 +1332,6 @@ def _collect_parameter_capabilities(instrument: Any) -> list[dict[str, Any]]:
13181332
if set_description is not None:
13191333
set_cmd["description"] = set_description
13201334

1321-
vals = None
1322-
if spec.vals is not None:
1323-
vals = {
1324-
"kind": spec.vals.kind,
1325-
"min_value": spec.vals.min_value,
1326-
"max_value": spec.vals.max_value,
1327-
"choices": list(spec.vals.choices),
1328-
}
1329-
13301335
safety = None
13311336
if spec.safety is not None:
13321337
safety = {
@@ -1342,15 +1347,11 @@ def _collect_parameter_capabilities(instrument: Any) -> list[dict[str, Any]]:
13421347
capability: dict[str, Any] = {
13431348
"label": spec.label,
13441349
"name": spec.name,
1345-
"unit": spec.unit,
1346-
"value_type": spec.value_type,
1347-
"snapshot_value": spec.snapshot_value,
13481350
"readable": bool(spec.readable),
13491351
"writable": bool(spec.writable),
13501352
"has_ramp": bool(spec.safety is not None and spec.safety.ramp_enabled),
13511353
"get_cmd": get_cmd,
13521354
"set_cmd": set_cmd,
1353-
"vals": vals,
13541355
"safety": safety,
13551356
}
13561357

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
from .extensions import (
22
ActionSpec,
33
ParameterSpec,
4-
ScalarParameterSpec,
54
load_action_specs,
65
load_parameter_specs,
7-
load_scalar_parameter_specs,
86
)
97
from .instrument import QcodesNanonisSTM
108

119
__all__ = [
1210
"QcodesNanonisSTM",
1311
"ActionSpec",
1412
"ParameterSpec",
15-
"ScalarParameterSpec",
1613
"load_action_specs",
1714
"load_parameter_specs",
18-
"load_scalar_parameter_specs",
1915
]

nanonis_qcodes_controller/qcodes_driver/extensions.py

Lines changed: 3 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping
4-
from dataclasses import dataclass, field
4+
from dataclasses import dataclass
55
from pathlib import Path
66
from typing import Any, Literal, cast
77

@@ -12,11 +12,9 @@
1212
DEFAULT_PARAMETERS_FILE = Path("config/parameters.yaml")
1313

1414
ScalarValueType = Literal["float", "int", "bool", "str"]
15-
ValidatorKind = Literal["numbers", "ints", "bool", "enum", "none"]
1615
ActionSafetyMode = Literal["alwaysAllowed", "guarded", "blocked"]
1716

1817
_ALLOWED_VALUE_TYPES: frozenset[str] = frozenset({"float", "int", "bool", "str"})
19-
_ALLOWED_VALIDATOR_KINDS: frozenset[str] = frozenset({"numbers", "ints", "bool", "enum", "none"})
2018
_ALLOWED_ACTION_SAFETY_MODES: frozenset[str] = frozenset({"alwaysAllowed", "guarded", "blocked"})
2119

2220

@@ -72,14 +70,6 @@ class ActionSpec:
7270
safety_mode: ActionSafetyMode = "guarded"
7371

7472

75-
@dataclass(frozen=True)
76-
class ValidatorSpec:
77-
kind: ValidatorKind
78-
min_value: float | None = None
79-
max_value: float | None = None
80-
choices: tuple[Any, ...] = ()
81-
82-
8373
@dataclass(frozen=True)
8474
class SafetySpec:
8575
min_value: float | None
@@ -95,13 +85,9 @@ class SafetySpec:
9585
class ParameterSpec:
9686
name: str
9787
label: str
98-
unit: str
99-
value_type: ScalarValueType
10088
get_cmd: ReadCommandSpec | None
10189
set_cmd: WriteCommandSpec | None
102-
vals: ValidatorSpec | None
10390
safety: SafetySpec | None
104-
snapshot_value: bool = True
10591
description: str = ""
10692

10793
@property
@@ -194,54 +180,31 @@ def _parse_parameter_spec(
194180
defaults: Mapping[str, Any],
195181
) -> ParameterSpec:
196182
label = str(mapping.get("label", name)).strip() or name
197-
unit = str(mapping.get("unit", "")).strip()
198183
description = str(mapping.get("description", "")).strip()
199184

200-
raw_value_type = mapping.get("value_type", mapping.get("type", "float"))
201-
value_type_text = str(raw_value_type).strip().lower()
202-
if value_type_text not in _ALLOWED_VALUE_TYPES:
203-
allowed = ", ".join(sorted(_ALLOWED_VALUE_TYPES))
204-
raise ValueError(
205-
f"parameters.{name}.value_type must be one of: {allowed}. Received: {raw_value_type}"
206-
)
207-
value_type = cast(ScalarValueType, value_type_text)
208-
209185
get_cmd = _parse_read_command(mapping.get("get_cmd"), context=f"parameters.{name}.get_cmd")
210186
set_cmd = _parse_write_command(mapping.get("set_cmd"), context=f"parameters.{name}.set_cmd")
211187

212188
if get_cmd is None and set_cmd is None:
213189
raise ValueError(f"Parameter '{name}' must define at least one of get_cmd or set_cmd.")
214190

215-
vals = _parse_vals(
216-
mapping.get("vals"), value_type=value_type, context=f"parameters.{name}.vals"
217-
)
218191
safety = _parse_safety(
219192
mapping.get("safety"),
220-
vals=vals,
221193
defaults=defaults,
222194
context=f"parameters.{name}.safety",
223195
writable=set_cmd is not None,
224196
)
225197

226-
snapshot_value = _parse_bool(
227-
mapping.get("snapshot_value", defaults.get("snapshot_value", True)),
228-
field_name=f"parameters.{name}.snapshot_value",
229-
)
230-
231198
if set_cmd is not None and safety is None:
232199
raise ValueError(f"Writable parameter '{name}' must include safety settings.")
233200

234201
return ParameterSpec(
235202
name=name,
236203
label=label,
237-
unit=unit,
238204
description=description,
239-
value_type=value_type,
240205
get_cmd=get_cmd,
241206
set_cmd=set_cmd,
242-
vals=vals,
243207
safety=safety,
244-
snapshot_value=snapshot_value,
245208
)
246209

247210

@@ -479,44 +442,9 @@ def _legacy_arg_fields_from_mapping(
479442
return tuple(fields)
480443

481444

482-
def _parse_vals(value: Any, *, value_type: ScalarValueType, context: str) -> ValidatorSpec | None:
483-
if value is None:
484-
if value_type == "bool":
485-
return ValidatorSpec(kind="bool")
486-
return None
487-
if value is False:
488-
return None
489-
490-
mapping = _as_mapping(value, context=context)
491-
kind_raw = mapping.get("kind", _default_validator_kind(value_type))
492-
kind = str(kind_raw).strip().lower()
493-
if kind not in _ALLOWED_VALIDATOR_KINDS:
494-
allowed = ", ".join(sorted(_ALLOWED_VALIDATOR_KINDS))
495-
raise ValueError(f"{context}.kind must be one of: {allowed}. Received: {kind_raw}")
496-
497-
min_value = None if mapping.get("min") is None else float(mapping["min"])
498-
max_value = None if mapping.get("max") is None else float(mapping["max"])
499-
if min_value is not None and max_value is not None and max_value < min_value:
500-
raise ValueError(f"{context}: max must be >= min.")
501-
502-
choices_raw = mapping.get("choices", ())
503-
if isinstance(choices_raw, (list, tuple)):
504-
choices = tuple(choices_raw)
505-
else:
506-
raise ValueError(f"{context}.choices must be a list when provided.")
507-
508-
return ValidatorSpec(
509-
kind=cast(ValidatorKind, kind),
510-
min_value=min_value,
511-
max_value=max_value,
512-
choices=choices,
513-
)
514-
515-
516445
def _parse_safety(
517446
value: Any,
518447
*,
519-
vals: ValidatorSpec | None,
520448
defaults: Mapping[str, Any],
521449
context: str,
522450
writable: bool,
@@ -531,8 +459,8 @@ def _parse_safety(
531459
if not writable and not mapping:
532460
return None
533461

534-
min_default = vals.min_value if vals is not None else None
535-
max_default = vals.max_value if vals is not None else None
462+
min_default = None
463+
max_default = None
536464
max_step_default = None
537465

538466
min_value_raw = mapping.get("min", min_default)
@@ -589,16 +517,6 @@ def _parse_safety(
589517
)
590518

591519

592-
def _default_validator_kind(value_type: ScalarValueType) -> ValidatorKind:
593-
if value_type == "float":
594-
return "numbers"
595-
if value_type == "int":
596-
return "ints"
597-
if value_type == "bool":
598-
return "bool"
599-
return "none"
600-
601-
602520
def _parse_required_string(value: Any, *, field_name: str) -> str:
603521
text = "" if value is None else str(value).strip()
604522
if not text:
@@ -627,70 +545,3 @@ def _parse_bool(value: Any, *, field_name: str) -> bool:
627545
return False
628546

629547
raise ValueError(f"Invalid boolean value for {field_name}: {value}")
630-
631-
632-
@dataclass(frozen=True)
633-
class ScalarParameterSpec:
634-
name: str
635-
command: str
636-
value_type: ScalarValueType = "float"
637-
unit: str = ""
638-
label: str | None = None
639-
payload_index: int = 0
640-
args: Mapping[str, Any] = field(default_factory=dict)
641-
snapshot_value: bool = True
642-
643-
644-
def load_scalar_parameter_specs(parameter_file: str | Path) -> tuple[ScalarParameterSpec, ...]:
645-
parameter_path = Path(parameter_file).expanduser()
646-
if not parameter_path.exists():
647-
raise ValueError(f"Parameter file does not exist: {parameter_path}")
648-
649-
with parameter_path.open("r", encoding="utf-8") as handle:
650-
loaded = yaml.safe_load(handle)
651-
652-
if loaded is None:
653-
return ()
654-
655-
root = _as_mapping(loaded, context="root")
656-
parameters_raw = root.get("parameters", [])
657-
if not isinstance(parameters_raw, list):
658-
raise ValueError("Parameter file field 'parameters' must be a list.")
659-
660-
specs: list[ScalarParameterSpec] = []
661-
for index, entry in enumerate(parameters_raw):
662-
context = f"parameters[{index}]"
663-
mapping = _as_mapping(entry, context=context)
664-
665-
raw_value_type = mapping.get("value_type", mapping.get("type", "float"))
666-
value_type_text = str(raw_value_type).strip().lower()
667-
if value_type_text not in _ALLOWED_VALUE_TYPES:
668-
allowed = ", ".join(sorted(_ALLOWED_VALUE_TYPES))
669-
raise ValueError(
670-
f"{context}.value_type must be one of: {allowed}. Received: {raw_value_type}"
671-
)
672-
673-
payload_index = int(mapping.get("payload_index", 0))
674-
if payload_index < 0:
675-
raise ValueError(f"{context}.payload_index must be non-negative.")
676-
677-
args = _as_mapping(mapping.get("args"), context=f"{context}.args")
678-
specs.append(
679-
ScalarParameterSpec(
680-
name=_parse_required_string(mapping.get("name"), field_name=f"{context}.name"),
681-
command=_parse_required_string(
682-
mapping.get("command"), field_name=f"{context}.command"
683-
),
684-
value_type=cast(ScalarValueType, value_type_text),
685-
unit=str(mapping.get("unit", "")).strip(),
686-
label=(None if mapping.get("label") is None else str(mapping.get("label")).strip()),
687-
payload_index=payload_index,
688-
args=dict(args),
689-
snapshot_value=_parse_bool(
690-
mapping.get("snapshot_value", True),
691-
field_name=f"{context}.snapshot_value",
692-
),
693-
)
694-
)
695-
696-
return tuple(specs)

0 commit comments

Comments
 (0)