plesty.lib.experiment.control ============================= .. py:module:: plesty.lib.experiment.control .. autoapi-nested-parse:: Driving a run that has already started, from inside it or from another process. A run is a process of its own: the console that started it cannot call a method on it. So the control surface is a file in the run directory — ``control.json``, written by whoever is driving and read by the run itself. The run directory is already the whole interface in this direction (a watcher tails ``journal.jsonl`` and ``records.jsonl`` and asks the run nothing), and a bench already stops a device server with a file rather than a signal, because Windows has no ``SIGINT`` to send. No socket, no bound port, and it crosses a master-to-node relay for free, since whoever runs the commands runs them on the host holding the files. **The step is the unit.** A request takes effect at a step boundary, which is where the journal already commits: a step is atomic, so there is no coherent place inside one to stop. :func:`abort` is the exception and says so — it interrupts the step in flight, which is the point of having it. .. code-block:: python from plesty.lib.experiment import control control.pause(run_dir) # stop before the next step control.report(run_dir, "helium top-up") # tell the run what happened control.resume(run_dir) control.cancel(run_dir) # finish this step, then stop The writers are plain functions because the caller is usually not holding an :class:`~plesty.lib.experiment.base_experiment.Experiment` — it is a console in another process with nothing but the path. Attributes ---------- .. autoapisummary:: plesty.lib.experiment.control.CONTROL_FILE plesty.lib.experiment.control.CANCEL plesty.lib.experiment.control.ABORT plesty.lib.experiment.control.POLL_SECONDS Classes ------- .. autoapisummary:: plesty.lib.experiment.control.ControlState plesty.lib.experiment.control.Control plesty.lib.experiment.control.MemoryControl plesty.lib.experiment.control.FileControl Functions --------- .. autoapisummary:: plesty.lib.experiment.control.state plesty.lib.experiment.control.pause plesty.lib.experiment.control.resume plesty.lib.experiment.control.cancel plesty.lib.experiment.control.abort plesty.lib.experiment.control.report plesty.lib.experiment.control.clear plesty.lib.experiment.control._read plesty.lib.experiment.control._amend Module Contents --------------- .. py:data:: CONTROL_FILE :value: 'control.json' .. py:data:: CANCEL :value: 'cancel' .. py:data:: ABORT :value: 'abort' .. py:data:: POLL_SECONDS :value: 0.5 .. py:class:: ControlState What the driver is currently asking of the run. :ivar paused: Whether the run should hold before its next step. :ivar stop: :data:`CANCEL`, :data:`ABORT`, or ``None`` to keep going. :ivar reports: Every report written so far, oldest first. The run consumes them by count, so a report is journaled once however often the file is read. :ivar reason: Why the stop was asked for, when the driver said. .. py:attribute:: paused :type: bool :value: False .. py:attribute:: stop :type: Optional[str] :value: None .. py:attribute:: reports :type: Sequence[str] :value: () .. py:attribute:: reason :type: str :value: '' .. py:property:: stopping :type: bool Whether a stop of either kind has been asked for. .. py:method:: to_dict() -> dict[str, Any] Return the state as JSON primitives. .. py:method:: from_dict(raw: Mapping[str, Any]) -> ControlState :classmethod: Rebuild a state from a control document. A malformed field takes its default rather than raising: this document is read on every step of a long measurement, and a run must not die because something wrote nonsense into it. :param raw: The parsed document. :returns: The state it describes. .. py:class:: Control Bases: :py:obj:`Protocol` Where a run reads what is being asked of it. .. py:method:: read() -> ControlState Return what the driver is asking for now. .. py:class:: MemoryControl A control surface held in memory, for a run driven in its own process. A notebook or a script that holds the experiment calls the methods directly; nothing is written to disk, so a run nobody drives from outside pays no file read per step. Start with a run that is asked for nothing. .. py:attribute:: state .. py:method:: read() -> ControlState Return the state as last set. .. py:method:: pause() -> None Ask the run to hold before its next step. .. py:method:: resume() -> None Let a held run go on. .. py:method:: cancel(reason: str = '') -> None Ask the run to finish the step in flight and then stop. .. py:method:: abort(reason: str = '') -> None Ask the run to stop without finishing the step in flight. .. py:method:: report(text: str) -> None Add a report for the run to journal. .. py:class:: FileControl(run_dir: str | os.PathLike[str]) A control surface in a run directory, for a run driven from elsewhere. Reading is deliberately forgiving: a missing file means nothing is being asked, and an unparseable one is treated the same way. The file is written by another process and may be read mid-write, and a measurement must not end because a read landed between a truncate and a flush — the next step reads it again a moment later. Watch the control file of *run_dir*. :param run_dir: The run's checkpoint directory. .. py:attribute:: path .. py:method:: read() -> ControlState Return what the file asks for, or a clear state when it says nothing. .. py:function:: state(run_dir: str | os.PathLike[str]) -> ControlState Return what is currently being asked of the run in *run_dir*. :param run_dir: The run's checkpoint directory. :returns: The control state; a clear one when no control file exists. .. py:function:: pause(run_dir: str | os.PathLike[str]) -> ControlState Ask the run to hold before its next step. The rig stays connected and the instruments keep their positions — that is the whole difference from stopping and resuming, which releases the rig and pays a reconnect and a preflight to come back. :param run_dir: The run's checkpoint directory. :returns: The state now asked for. .. py:function:: resume(run_dir: str | os.PathLike[str]) -> ControlState Let a held run go on. :param run_dir: The run's checkpoint directory. :returns: The state now asked for. .. py:function:: cancel(run_dir: str | os.PathLike[str], reason: str = '') -> ControlState Ask the run to finish the step in flight and then stop. Nothing is lost and nothing is redone: the step commits, ``teardown`` runs and the journal records ``run_canceled``, so a resume starts at the next step. The cost is waiting for the step — which on a long exposure or a magnet ramp is real; that is what :func:`abort` is for. :param run_dir: The run's checkpoint directory. :param reason: Why, journaled with the event. :returns: The state now asked for. .. py:function:: abort(run_dir: str | os.PathLike[str], reason: str = '') -> ControlState Ask the run to stop without finishing the step in flight. The operation in flight is interrupted and ``teardown`` runs, so the instruments are left safe rather than mid-acquisition. The step never committed, so a resume re-runs it from the beginning. :param run_dir: The run's checkpoint directory. :param reason: Why, journaled with the event. :returns: The state now asked for. .. py:function:: report(run_dir: str | os.PathLike[str], text: str) -> ControlState Tell the running run something worth recording. The run journals it where it happened — between two steps — so a helium top-up or a realignment is on record beside the data it explains, and reaches the measurement report rather than the operator's memory. :param run_dir: The run's checkpoint directory. :param text: What to record. :returns: The state now asked for. .. py:function:: clear(run_dir: str | os.PathLike[str]) -> None Remove the control file, so the run is asked for nothing. :param run_dir: The run's checkpoint directory. .. py:function:: _read(path: pathlib.Path) -> dict[str, Any] Return the parsed control document, or an empty one. :param path: The control file. :returns: The document; ``{}`` when it is absent, unreadable or not a mapping. .. py:function:: _amend(run_dir: str | os.PathLike[str], **changes: Any) -> ControlState Apply *changes* to the control file and return the resulting state. The file is replaced rather than rewritten in place, so a run reading it sees either the old document or the new one and never half of each. :param run_dir: The run's checkpoint directory. :param \*\*changes: Fields of the control document to set. :returns: The state the file now asks for.