rhylthyme-cli-runner 0.2.7a0__tar.gz → 0.2.8a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rhylthyme_cli_runner-0.2.7a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.8a0}/PKG-INFO +4 -2
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/setup.py +4 -2
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/cli.py +67 -4
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/recorder.py +36 -0
- rhylthyme_cli_runner-0.2.8a0/src/rhylthyme_cli_runner/instruments.py +258 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/program_planner.py +30 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/program_runner.py +305 -6
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/remote/cli.py +23 -1
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/validate_program.py +19 -7
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +4 -2
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +2 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +4 -1
- rhylthyme_cli_runner-0.2.8a0/tests/test_instruments.py +1129 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_remote.py +62 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_validate_program.py +8 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/LICENSE +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/README.md +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/pyproject.toml +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/setup.cfg +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/render.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/import_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/render_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_eval_baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_eval_harness.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_eval_providers.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_eval_scorer.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_example_validation.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_examples_integration.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_hash_parity.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_history_report.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_import_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_mcp_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_planner_time_strings.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_predict.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_program_runner.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_render_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_replay.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_run_recorder.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_skill.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_synth.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_usable.py +0 -0
- {rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/tests/test_validate_examples_ci.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhylthyme-cli-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8a0
|
|
4
4
|
Summary: CLI runner for Rhylthyme real-time program schedules
|
|
5
5
|
Home-page: https://github.com/rhylthyme/rhylthyme-cli-runner
|
|
6
6
|
Author: Rhylthyme Team
|
|
@@ -26,7 +26,9 @@ Requires-Dist: click>=8.0.0
|
|
|
26
26
|
Requires-Dist: jsonschema>=4.0.0
|
|
27
27
|
Requires-Dist: pyyaml>=6.0
|
|
28
28
|
Requires-Dist: colorama>=0.4.0
|
|
29
|
-
Requires-Dist: rhylthyme-spec>=0.2.
|
|
29
|
+
Requires-Dist: rhylthyme-spec>=0.2.1a0
|
|
30
|
+
Provides-Extra: galago
|
|
31
|
+
Requires-Dist: rhylthyme-galago>=0.1.0a0; extra == "galago"
|
|
30
32
|
Provides-Extra: dev
|
|
31
33
|
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
|
32
34
|
Requires-Dist: pytest-cov>=2.10.0; extra == "dev"
|
|
@@ -19,7 +19,7 @@ def read_readme():
|
|
|
19
19
|
|
|
20
20
|
setup(
|
|
21
21
|
name="rhylthyme-cli-runner",
|
|
22
|
-
version="0.2.
|
|
22
|
+
version="0.2.8a0",
|
|
23
23
|
description="CLI runner for Rhylthyme real-time program schedules",
|
|
24
24
|
long_description=read_readme(),
|
|
25
25
|
long_description_content_type="text/markdown",
|
|
@@ -38,9 +38,11 @@ setup(
|
|
|
38
38
|
"jsonschema>=4.0.0",
|
|
39
39
|
"pyyaml>=6.0",
|
|
40
40
|
"colorama>=0.4.0",
|
|
41
|
-
"rhylthyme-spec>=0.2.
|
|
41
|
+
"rhylthyme-spec>=0.2.1a0",
|
|
42
42
|
],
|
|
43
43
|
extras_require={
|
|
44
|
+
# Instrument steps on galago-tools lab instruments (--workcell)
|
|
45
|
+
"galago": ["rhylthyme-galago>=0.1.0a0"],
|
|
44
46
|
"dev": [
|
|
45
47
|
# Testing framework
|
|
46
48
|
"pytest>=6.0.0",
|
{rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/__init__.py
RENAMED
|
@@ -6,7 +6,7 @@ This package provides the command-line interface for running and validating
|
|
|
6
6
|
Rhylthyme real-time program schedules.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.2.
|
|
9
|
+
__version__ = "0.2.8a0"
|
|
10
10
|
__author__ = "Rhylthyme Team"
|
|
11
11
|
__description__ = "CLI runner for Rhylthyme real-time program schedules"
|
|
12
12
|
|
{rhylthyme_cli_runner-0.2.7a0 → rhylthyme_cli_runner-0.2.8a0}/src/rhylthyme_cli_runner/cli.py
RENAMED
|
@@ -105,7 +105,18 @@ def cli(ctx, environments_dir):
|
|
|
105
105
|
is_flag=True,
|
|
106
106
|
help="Enforce all tasks must be defined in resourceConstraints (strict mode)",
|
|
107
107
|
)
|
|
108
|
-
|
|
108
|
+
@click.option(
|
|
109
|
+
"--workcell",
|
|
110
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
111
|
+
default=None,
|
|
112
|
+
help=(
|
|
113
|
+
"Workcell file: check instrument steps against its galago tools "
|
|
114
|
+
"(tool names, types, commands and params)"
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
def validate(
|
|
118
|
+
program_files, schema, environment, verbose, json_output, strict, workcell
|
|
119
|
+
):
|
|
109
120
|
"""
|
|
110
121
|
Validate one or more program files against the schema.
|
|
111
122
|
|
|
@@ -115,6 +126,8 @@ def validate(program_files, schema, environment, verbose, json_output, strict):
|
|
|
115
126
|
Use --json to get machine-readable output for CI or scripting.
|
|
116
127
|
Use --strict to require all tasks used in steps/buffers to be defined in resourceConstraints.
|
|
117
128
|
Use -e/--environment to validate against specific environment constraints.
|
|
129
|
+
Use --workcell to check instrument steps against a lab's galago tools;
|
|
130
|
+
without it, commands are checked against each step's toolType.
|
|
118
131
|
"""
|
|
119
132
|
# Set up environment for validation if specified
|
|
120
133
|
if environment:
|
|
@@ -165,7 +178,7 @@ def validate(program_files, schema, environment, verbose, json_output, strict):
|
|
|
165
178
|
all_valid = True
|
|
166
179
|
for program_file in program_files:
|
|
167
180
|
success = validate_program_file(
|
|
168
|
-
program_file, schema, verbose, json_output, strict
|
|
181
|
+
program_file, schema, verbose, json_output, strict, workcell
|
|
169
182
|
)
|
|
170
183
|
if not success:
|
|
171
184
|
all_valid = False
|
|
@@ -256,6 +269,28 @@ def validate(program_files, schema, environment, verbose, json_output, strict):
|
|
|
256
269
|
"answers, which are what the record stores in context.userTags"
|
|
257
270
|
),
|
|
258
271
|
)
|
|
272
|
+
@click.option(
|
|
273
|
+
"--workcell",
|
|
274
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
275
|
+
default=None,
|
|
276
|
+
help=(
|
|
277
|
+
"Workcell file mapping the program's instrument tools to galago-tools "
|
|
278
|
+
"servers; required for programs with instrument steps"
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
@click.option(
|
|
282
|
+
"--live",
|
|
283
|
+
is_flag=True,
|
|
284
|
+
help=(
|
|
285
|
+
"Run instrument steps on real hardware instead of galago's simulated "
|
|
286
|
+
"mode; shows what will run and asks first"
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
@click.option(
|
|
290
|
+
"--confirm-live",
|
|
291
|
+
is_flag=True,
|
|
292
|
+
help="Answer the --live question in advance (for scripts)",
|
|
293
|
+
)
|
|
259
294
|
def run(
|
|
260
295
|
program_file,
|
|
261
296
|
schema,
|
|
@@ -270,6 +305,9 @@ def run(
|
|
|
270
305
|
history_file,
|
|
271
306
|
no_history,
|
|
272
307
|
predict_context,
|
|
308
|
+
workcell,
|
|
309
|
+
live,
|
|
310
|
+
confirm_live,
|
|
273
311
|
):
|
|
274
312
|
"""
|
|
275
313
|
Run a program file with the interactive UI.
|
|
@@ -296,6 +334,11 @@ def run(
|
|
|
296
334
|
--history FILE to read a specific corpus, --no-history to turn it off,
|
|
297
335
|
and --predict-context KEY=VALUE to predict for a context other than the
|
|
298
336
|
factor answers.
|
|
337
|
+
|
|
338
|
+
Programs with instrument steps need --workcell FILE (galago-tools servers,
|
|
339
|
+
via rhylthyme-galago). Tools run in galago's simulated mode unless you pass
|
|
340
|
+
--live, which lists the tools and commands and asks you to type 'live'
|
|
341
|
+
before anything is configured; --confirm-live answers for scripts.
|
|
299
342
|
"""
|
|
300
343
|
run_program(
|
|
301
344
|
program_file,
|
|
@@ -311,6 +354,9 @@ def run(
|
|
|
311
354
|
history_file=history_file,
|
|
312
355
|
use_history=not no_history,
|
|
313
356
|
predict_context=predict_context,
|
|
357
|
+
workcell=workcell,
|
|
358
|
+
live=live,
|
|
359
|
+
confirm_live=confirm_live,
|
|
314
360
|
)
|
|
315
361
|
|
|
316
362
|
|
|
@@ -324,7 +370,16 @@ def run(
|
|
|
324
370
|
@click.option(
|
|
325
371
|
"--verbose", "-v", is_flag=True, help="Show detailed planning information"
|
|
326
372
|
)
|
|
327
|
-
|
|
373
|
+
@click.option(
|
|
374
|
+
"--workcell",
|
|
375
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
376
|
+
default=None,
|
|
377
|
+
help=(
|
|
378
|
+
"Workcell file: estimate instrument steps that have no duration from "
|
|
379
|
+
"their tools' EstimateDuration"
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
def plan(input_file, output_file, environment, verbose, workcell):
|
|
328
383
|
"""
|
|
329
384
|
Optimize a program schedule to reduce resource contention.
|
|
330
385
|
|
|
@@ -333,9 +388,17 @@ def plan(input_file, output_file, environment, verbose):
|
|
|
333
388
|
starts to reduce contention at critical junctures.
|
|
334
389
|
|
|
335
390
|
The optimized program is saved to the specified output file.
|
|
391
|
+
|
|
392
|
+
Instrument steps without a duration get an estimated one (flagged in
|
|
393
|
+
metadata.durationEstimate): from the tool with --workcell, else from a
|
|
394
|
+
duration-like command param, else a default.
|
|
336
395
|
"""
|
|
337
396
|
success = plan_program(
|
|
338
|
-
input_file,
|
|
397
|
+
input_file,
|
|
398
|
+
output_file,
|
|
399
|
+
verbose,
|
|
400
|
+
environment_file=environment,
|
|
401
|
+
workcell=workcell,
|
|
339
402
|
)
|
|
340
403
|
if not success:
|
|
341
404
|
sys.exit(1)
|
|
@@ -13,6 +13,8 @@ the events it emits are:
|
|
|
13
13
|
``step_aborted`` {step_id, time, reason}
|
|
14
14
|
``program_paused`` {time, wall_time}
|
|
15
15
|
``program_resumed`` {time, wall_time, paused_seconds}
|
|
16
|
+
``program_aborted`` {time, reason} (reason goes to ``context.abortReason``)
|
|
17
|
+
``instrument_reply`` {step_id, time, ok, code, errorMessage, metadata}
|
|
16
18
|
|
|
17
19
|
All ``time`` values are the runner's program clock (an epoch float that
|
|
18
20
|
advances at ``time_scale``); the record stores them as seconds from the
|
|
@@ -217,6 +219,8 @@ class RunRecorder:
|
|
|
217
219
|
self.context: Dict[str, Any] = self._build_context()
|
|
218
220
|
|
|
219
221
|
self.actuals: Dict[str, Dict[str, Any]] = {}
|
|
222
|
+
# step id -> {tool, command, replies: [{attempt, at, code, ...}]}
|
|
223
|
+
self.instrument_log: Dict[str, Dict[str, Any]] = {}
|
|
220
224
|
self.paused_seconds: Dict[str, float] = {sid: 0.0 for sid in self.step_order}
|
|
221
225
|
self._pause_wall: Optional[float] = None
|
|
222
226
|
self._paused_running: List[str] = []
|
|
@@ -271,6 +275,31 @@ class RunRecorder:
|
|
|
271
275
|
entry.setdefault("start", data["time"])
|
|
272
276
|
entry["end"] = data["time"]
|
|
273
277
|
entry["endedBy"] = "abort"
|
|
278
|
+
elif event_type == "instrument_reply":
|
|
279
|
+
sid = data["step_id"]
|
|
280
|
+
step = getattr(self.runner, "steps", {}).get(sid)
|
|
281
|
+
instrument = (getattr(step, "instrument", None) or {}) if step else {}
|
|
282
|
+
log = self.instrument_log.setdefault(
|
|
283
|
+
sid,
|
|
284
|
+
{
|
|
285
|
+
"tool": str(instrument.get("tool", "")),
|
|
286
|
+
"command": str(instrument.get("command", "")),
|
|
287
|
+
"replies": [],
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
reply: Dict[str, Any] = {
|
|
291
|
+
"attempt": getattr(step, "instrument_attempts", 0)
|
|
292
|
+
or len(log["replies"]) + 1,
|
|
293
|
+
"at": data["time"],
|
|
294
|
+
"code": str(data.get("code", "")),
|
|
295
|
+
}
|
|
296
|
+
if data.get("errorMessage"):
|
|
297
|
+
reply["errorMessage"] = str(data["errorMessage"])
|
|
298
|
+
if data.get("metadata"):
|
|
299
|
+
reply["metadata"] = copy.deepcopy(data["metadata"])
|
|
300
|
+
log["replies"].append(reply)
|
|
301
|
+
elif event_type == "program_aborted":
|
|
302
|
+
self.context["abortReason"] = data.get("reason") or "aborted"
|
|
274
303
|
elif event_type == "program_paused":
|
|
275
304
|
self._pause_wall = data.get("wall_time", time.time())
|
|
276
305
|
self._paused_running = list(getattr(self.runner, "running_steps", []))
|
|
@@ -344,6 +373,13 @@ class RunRecorder:
|
|
|
344
373
|
entry["endedBy"] = actual.get("endedBy", "executor")
|
|
345
374
|
if "triggerFiredAt" in actual:
|
|
346
375
|
entry["triggerFiredAt"] = rel(actual["triggerFiredAt"])
|
|
376
|
+
log = self.instrument_log.get(sid)
|
|
377
|
+
if log:
|
|
378
|
+
entry["instrument"] = {
|
|
379
|
+
"tool": log["tool"],
|
|
380
|
+
"command": log["command"],
|
|
381
|
+
"replies": [dict(r, at=rel(r["at"])) for r in log["replies"]],
|
|
382
|
+
}
|
|
347
383
|
steps.append(entry)
|
|
348
384
|
|
|
349
385
|
started_iso = _iso_utc(start_epoch) if start_epoch is not None else None
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connect a ProgramRunner to galago-tools instruments (``rhylthyme run --workcell``).
|
|
3
|
+
|
|
4
|
+
Needs the optional ``rhylthyme-galago`` package (``pip install
|
|
5
|
+
rhylthyme[galago]``). When a step with an ``instrument`` starts, its command is
|
|
6
|
+
sent on a worker thread; the reply comes back through
|
|
7
|
+
``ProgramRunner.post_instrument_reply`` and ends (or fails) the step.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from .instance_checks import Finding
|
|
13
|
+
|
|
14
|
+
INSTALL_HINT = "Instrument steps need rhylthyme-galago: pip install 'rhylthyme[galago]'"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InstrumentSetupError(RuntimeError):
|
|
18
|
+
"""The workcell cannot run this program's instrument steps."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def program_uses_instruments(program: Mapping[str, Any]) -> bool:
|
|
22
|
+
return any(
|
|
23
|
+
step.get("instrument")
|
|
24
|
+
for track in program.get("tracks", [])
|
|
25
|
+
for step in track.get("steps", [])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def instrument_findings(
|
|
30
|
+
program: Mapping[str, Any], workcell_source=None
|
|
31
|
+
) -> List[Finding]:
|
|
32
|
+
"""
|
|
33
|
+
Validation findings for a program's instrument steps: galago commands and
|
|
34
|
+
params checked against each step's toolType, or against the workcell's
|
|
35
|
+
tools when ``workcell_source`` (a path or dict) is given.
|
|
36
|
+
"""
|
|
37
|
+
if workcell_source is None and not program_uses_instruments(program):
|
|
38
|
+
return []
|
|
39
|
+
try:
|
|
40
|
+
import rhylthyme_galago as galago
|
|
41
|
+
except ImportError:
|
|
42
|
+
return [
|
|
43
|
+
Finding(
|
|
44
|
+
code="instrument_unchecked",
|
|
45
|
+
message="Instrument steps were not checked",
|
|
46
|
+
where="program",
|
|
47
|
+
fix=INSTALL_HINT,
|
|
48
|
+
severity="error" if workcell_source is not None else "warning",
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
workcell = None
|
|
52
|
+
if workcell_source is not None:
|
|
53
|
+
try:
|
|
54
|
+
workcell = galago.load_workcell(workcell_source)
|
|
55
|
+
except galago.WorkcellError as e:
|
|
56
|
+
return [Finding(code="workcell_invalid", message=str(e), where="workcell")]
|
|
57
|
+
return [
|
|
58
|
+
Finding(
|
|
59
|
+
code=issue.code,
|
|
60
|
+
message=issue.message,
|
|
61
|
+
where=f"step:{issue.step_id}",
|
|
62
|
+
severity=issue.severity,
|
|
63
|
+
)
|
|
64
|
+
for issue in galago.check_program(program, workcell)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def fill_instrument_durations(
|
|
69
|
+
program: Dict[str, Any], workcell_source=None
|
|
70
|
+
) -> Tuple[Dict[str, Any], List[str]]:
|
|
71
|
+
"""
|
|
72
|
+
Give instrument steps without a duration an estimated one, for planning:
|
|
73
|
+
the tool's EstimateDuration (with a workcell), else a duration-like
|
|
74
|
+
command param, else a default. Returns the new program and report lines;
|
|
75
|
+
without rhylthyme-galago the program comes back unchanged with a note.
|
|
76
|
+
"""
|
|
77
|
+
missing = [step for step in _instrument_steps(program) if "duration" not in step]
|
|
78
|
+
if not missing:
|
|
79
|
+
return program, []
|
|
80
|
+
try:
|
|
81
|
+
import rhylthyme_galago as galago
|
|
82
|
+
from rhylthyme_galago.estimates import describe
|
|
83
|
+
except ImportError:
|
|
84
|
+
return program, [
|
|
85
|
+
f"{len(missing)} instrument step(s) have no duration and were not "
|
|
86
|
+
f"estimated. {INSTALL_HINT}"
|
|
87
|
+
]
|
|
88
|
+
workcell = None
|
|
89
|
+
if workcell_source is not None:
|
|
90
|
+
try:
|
|
91
|
+
workcell = galago.load_workcell(workcell_source)
|
|
92
|
+
except galago.WorkcellError as e:
|
|
93
|
+
raise InstrumentSetupError(str(e)) from None
|
|
94
|
+
filled, estimates = galago.fill_durations(program, workcell)
|
|
95
|
+
return filled, ["Estimated instrument durations:"] + describe(estimates)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _reply_dict(reply) -> Dict[str, Any]:
|
|
99
|
+
return {
|
|
100
|
+
"ok": reply.ok,
|
|
101
|
+
"code": reply.code,
|
|
102
|
+
"errorMessage": reply.error_message,
|
|
103
|
+
"metadata": dict(reply.metadata),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _instrument_steps(program: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
|
108
|
+
return [
|
|
109
|
+
step
|
|
110
|
+
for track in program.get("tracks", [])
|
|
111
|
+
for step in track.get("steps", [])
|
|
112
|
+
if step.get("instrument")
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _params_text(params: Optional[Mapping[str, Any]]) -> str:
|
|
117
|
+
return ", ".join(f"{k}={v!r}" for k, v in (params or {}).items())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InstrumentSession:
|
|
121
|
+
"""
|
|
122
|
+
The workcell tools one run uses. Opening it touches no instrument;
|
|
123
|
+
``summary()`` only reads tool status; ``prepare()`` configures the tools
|
|
124
|
+
(simulated unless the session is live); ``attach()`` wires the runner so
|
|
125
|
+
each instrument step's command is sent when the step starts.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, executor: Any, tools: List[str]):
|
|
129
|
+
self.executor = executor
|
|
130
|
+
self.tools = tools
|
|
131
|
+
self.checks: List[Any] = []
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def live(self) -> bool:
|
|
135
|
+
return not self.executor.simulated
|
|
136
|
+
|
|
137
|
+
def summary(self, program: Mapping[str, Any], limit: int = 12) -> List[str]:
|
|
138
|
+
"""What a run will do, for the --live confirmation. Status is read-only."""
|
|
139
|
+
workcell = self.executor.workcell
|
|
140
|
+
mode = "LIVE: real hardware will move" if self.live else "simulated"
|
|
141
|
+
lines = [f"Workcell {workcell.id!r} ({mode})", "Tools:"]
|
|
142
|
+
for name in self.tools:
|
|
143
|
+
binding = workcell.tool(name)
|
|
144
|
+
status = self.executor.client(name).status()
|
|
145
|
+
lines.append(
|
|
146
|
+
f" {name} ({binding.type}) @ {binding.address}: {status.status}"
|
|
147
|
+
)
|
|
148
|
+
steps = _instrument_steps(program)
|
|
149
|
+
lines.append(f"Instrument steps ({len(steps)}):")
|
|
150
|
+
for step in steps[:limit]:
|
|
151
|
+
inst = step["instrument"]
|
|
152
|
+
lines.append(
|
|
153
|
+
f" {step['stepId']}: {inst['tool']}.{inst['command']}"
|
|
154
|
+
f"({_params_text(inst.get('params'))})"
|
|
155
|
+
)
|
|
156
|
+
if len(steps) > limit:
|
|
157
|
+
lines.append(f" ... and {len(steps) - limit} more")
|
|
158
|
+
return lines
|
|
159
|
+
|
|
160
|
+
def prepare(self) -> None:
|
|
161
|
+
"""Configure every tool; raise InstrumentSetupError unless all are ready."""
|
|
162
|
+
self.checks = self.executor.prepare(self.tools)
|
|
163
|
+
if not all(check.ready for check in self.checks):
|
|
164
|
+
raise InstrumentSetupError("\n".join(self.report()))
|
|
165
|
+
|
|
166
|
+
def report(self) -> List[str]:
|
|
167
|
+
mode = "simulated" if self.executor.simulated else "LIVE"
|
|
168
|
+
lines = [f"Workcell {self.executor.workcell.id!r} ({mode}):"]
|
|
169
|
+
for check in self.checks:
|
|
170
|
+
mark = "ok" if check.ready else "NOT READY"
|
|
171
|
+
detail = check.configure.error_message or check.status.error_message
|
|
172
|
+
line = f" {check.tool} @ {check.address}: {check.status.status} [{mark}]"
|
|
173
|
+
lines.append(line + (f" {detail}" if detail and not check.ready else ""))
|
|
174
|
+
return lines
|
|
175
|
+
|
|
176
|
+
def attach(self, runner) -> None:
|
|
177
|
+
executor = self.executor
|
|
178
|
+
|
|
179
|
+
def on_reply(step_id: str, reply) -> None:
|
|
180
|
+
runner.post_instrument_reply(step_id, _reply_dict(reply))
|
|
181
|
+
|
|
182
|
+
def on_event(event_type: str, data: Dict[str, Any]) -> None:
|
|
183
|
+
# A step's command goes out when it starts and again on each retry
|
|
184
|
+
if event_type not in ("step_started", "step_retry"):
|
|
185
|
+
return
|
|
186
|
+
step = runner.steps.get(data["step_id"])
|
|
187
|
+
if step is not None and step.instrument:
|
|
188
|
+
step.instrument_attempts += 1
|
|
189
|
+
executor.submit(step.step_id, step.instrument, on_reply)
|
|
190
|
+
|
|
191
|
+
runner.add_event_listener(on_event)
|
|
192
|
+
|
|
193
|
+
def shutdown(self) -> List[str]:
|
|
194
|
+
"""Stop the executor; return steps whose commands were still in flight."""
|
|
195
|
+
return self.executor.shutdown()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def open_instruments(
|
|
199
|
+
program: Mapping[str, Any],
|
|
200
|
+
workcell_source,
|
|
201
|
+
*,
|
|
202
|
+
client_factory: Optional[Callable] = None,
|
|
203
|
+
live: bool = False,
|
|
204
|
+
) -> InstrumentSession:
|
|
205
|
+
"""
|
|
206
|
+
Load the workcell for ``program`` without contacting any tool.
|
|
207
|
+
|
|
208
|
+
Tools run simulated unless ``live`` is true. Raises InstrumentSetupError
|
|
209
|
+
when rhylthyme-galago is missing, the workcell is invalid, or a step
|
|
210
|
+
names a tool the workcell lacks.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
import rhylthyme_galago as galago
|
|
214
|
+
except ImportError:
|
|
215
|
+
raise InstrumentSetupError(INSTALL_HINT) from None
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
workcell = galago.load_workcell(workcell_source)
|
|
219
|
+
except galago.WorkcellError as e:
|
|
220
|
+
raise InstrumentSetupError(str(e)) from None
|
|
221
|
+
|
|
222
|
+
tools = galago.instrument_tools(program)
|
|
223
|
+
unknown = [t for t in tools if t not in workcell.tools]
|
|
224
|
+
if unknown:
|
|
225
|
+
raise InstrumentSetupError(
|
|
226
|
+
f"Workcell {workcell.id!r} has no tool named "
|
|
227
|
+
+ ", ".join(repr(t) for t in unknown)
|
|
228
|
+
+ f" (tools: {', '.join(sorted(workcell.tools))})"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
kwargs: Dict[str, Any] = {"simulated": not live}
|
|
232
|
+
if client_factory is not None:
|
|
233
|
+
kwargs["client_factory"] = client_factory
|
|
234
|
+
return InstrumentSession(galago.InstrumentExecutor(workcell, **kwargs), tools)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def attach_instruments(
|
|
238
|
+
runner,
|
|
239
|
+
workcell_source,
|
|
240
|
+
*,
|
|
241
|
+
client_factory: Optional[Callable] = None,
|
|
242
|
+
live: bool = False,
|
|
243
|
+
) -> InstrumentSession:
|
|
244
|
+
"""
|
|
245
|
+
Open, configure and attach in one go (no confirmation step): the tools
|
|
246
|
+
are configured and ``runner`` sends each instrument step's command when
|
|
247
|
+
the step starts.
|
|
248
|
+
"""
|
|
249
|
+
session = open_instruments(
|
|
250
|
+
runner.program, workcell_source, client_factory=client_factory, live=live
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
session.prepare()
|
|
254
|
+
except InstrumentSetupError:
|
|
255
|
+
session.shutdown()
|
|
256
|
+
raise
|
|
257
|
+
session.attach(runner)
|
|
258
|
+
return session
|
|
@@ -16,6 +16,8 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
|
16
16
|
|
|
17
17
|
import yaml
|
|
18
18
|
|
|
19
|
+
from .instruments import fill_instrument_durations
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
def load_program_file(file_path: str) -> dict:
|
|
21
23
|
"""
|
|
@@ -965,11 +967,30 @@ def save_optimized_program(program: dict, output_file: str):
|
|
|
965
967
|
json.dump(program, f, indent=2)
|
|
966
968
|
|
|
967
969
|
|
|
970
|
+
def planned_makespan(program: dict) -> float:
|
|
971
|
+
"""
|
|
972
|
+
Seconds from program start to the end of the last step, following start
|
|
973
|
+
triggers and durations (resource waits not included). Uses the
|
|
974
|
+
validator's trigger arithmetic, which reads the current trackId/stepId
|
|
975
|
+
schema.
|
|
976
|
+
"""
|
|
977
|
+
from .validate_program import calculate_step_start_time, parse_duration_to_seconds
|
|
978
|
+
|
|
979
|
+
end = 0.0
|
|
980
|
+
for track in program.get("tracks", []):
|
|
981
|
+
steps = track.get("steps", [])
|
|
982
|
+
for step in steps:
|
|
983
|
+
start = calculate_step_start_time(step, steps, program)
|
|
984
|
+
end = max(end, start + parse_duration_to_seconds(step.get("duration")))
|
|
985
|
+
return end
|
|
986
|
+
|
|
987
|
+
|
|
968
988
|
def plan_program(
|
|
969
989
|
input_file: str,
|
|
970
990
|
output_file: str,
|
|
971
991
|
verbose: bool = False,
|
|
972
992
|
environment_file: Optional[str] = None,
|
|
993
|
+
workcell: Optional[str] = None,
|
|
973
994
|
) -> bool:
|
|
974
995
|
"""
|
|
975
996
|
Plan and optimize a program schedule.
|
|
@@ -979,18 +1000,27 @@ def plan_program(
|
|
|
979
1000
|
output_file: Path to the output program file
|
|
980
1001
|
verbose: Whether to print verbose information
|
|
981
1002
|
environment_file: Path to the environment file
|
|
1003
|
+
workcell: Workcell file; instrument steps without a duration are
|
|
1004
|
+
estimated from its tools (else from their params, else a default)
|
|
982
1005
|
|
|
983
1006
|
Returns:
|
|
984
1007
|
True if successful, False otherwise
|
|
985
1008
|
"""
|
|
986
1009
|
try:
|
|
987
1010
|
program = load_program_file(input_file)
|
|
1011
|
+
program, estimate_lines = fill_instrument_durations(program, workcell)
|
|
1012
|
+
for line in estimate_lines:
|
|
1013
|
+
print(line)
|
|
988
1014
|
environment = None
|
|
989
1015
|
if environment_file:
|
|
990
1016
|
environment = load_program_file(environment_file)
|
|
991
1017
|
planner = ProgramPlanner(program, verbose, environment)
|
|
992
1018
|
optimized_program = planner.optimize_schedule()
|
|
993
1019
|
save_optimized_program(optimized_program, output_file)
|
|
1020
|
+
print(
|
|
1021
|
+
"Makespan (by start triggers and durations): "
|
|
1022
|
+
f"{planned_makespan(optimized_program):g} s"
|
|
1023
|
+
)
|
|
994
1024
|
if verbose:
|
|
995
1025
|
print(f"Saved optimized program to {output_file}")
|
|
996
1026
|
return True
|