Skip to content

FE-1162: Python optimisation script (Optuna) driving the Petrinaut CLI#8989

Open
YannisZa wants to merge 19 commits into
cf-petrinaut-pythonfrom
python-opt-api
Open

FE-1162: Python optimisation script (Optuna) driving the Petrinaut CLI#8989
YannisZa wants to merge 19 commits into
cf-petrinaut-pythonfrom
python-opt-api

Conversation

@YannisZa

@YannisZa YannisZa commented Jul 9, 2026

Copy link
Copy Markdown

🌟 What is the purpose of this PR?

Adds @hashintel/petrinaut-opt, a standalone Python service for black-box optimization of Petri-net executions. Given a set of Petri-net parameters and initial states (markings), it uses Optuna to search the input space and maximise/minimise a single metric computed by the Petri-net model. Each candidate is evaluated by running the net through the petrinaut-cli socket server, and results stream back one trial at a time over Server-Sent Events so a UI can watch the optimization live.

This is the Python-side counterpart to the Petrinaut CLI server: the CLI executes the net, this package decides which inputs to try and orchestrates the search.

🔗 Related links

Depends on the Petrinaut CLI socket server (petrinaut serve) — libs/@hashintel/petrinaut-cli

  • ...

🚫 Blocked by

None

🔍 What does this change?

Adds a new Python (uv) package petrinaut-opt under libs/@hashintel/.
petrinaut_client.py — PetrinautModel connects to the Petrinaut CLI over a Unix socket (/tmp/petrinaut.sock) and, per trial, sends a run request (parameters, initialState, metrics, maxSteps, dt, seed) and reads back the named metric. PetrinautModelSpec (Pydantic) configures the execution.
petrinaut_optimizer.py — PetrinautOptimizer drives an Optuna study: suggest proposes inputs (fixed inputs held constant, the rest searched over BOUNDS), objective runs the model and prunes failed/errored trials, and stream_all / stream_best emit SSE frames (one per finished trial / running-best) via a background thread and asyncio.Queue. OptimizationSpec configures a run.
optimization_api.py — FastAPI service exposing POST /init (create a session), GET /optimize/{session_id}/stream/all, GET /optimize/{session_id}/stream/best, DELETE /optimize/{session_id}, GET /status, and GET /.
Concurrency is scoped per session: each optimizer holds its own lock (one session can't be driven by two concurrent streams), and there is no global single-run guard — independent /init sessions run independently.
stream_best only emits once at least one trial has completed, avoiding an Optuna error when opening trials are all pruned.
Adds README.md, pyproject.toml, requirements.txt, and .gitignore.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing
  • modifies an npm-publishable library and I have added a changeset file(s)
  • modifies a Cargo-publishable library and I have amended the version
  • modifies a Cargo-publishable library, but it is not yet ready to publish
  • modifies a block that will need publishing via GitHub action once merged
  • I am unsure / need advice

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change
  • are in a state where docs changes are not yet required but will be
  • require changes to docs which are made as part of this PR
  • require changes to docs which are not made in this PR
    • Provide more detail here
  • I am unsure / need advice

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph
  • affected the execution graph, and the turbo.json's have been updated to reflect this
  • I am unsure / need advice

⚠️ Known issues

Socket path /tmp/petrinaut.sock is hard-coded on both the client and the CLI side; they must be started to match.
If an entire input group (parameters or initial_states) is fully fixed, the SSE callback can raise a KeyError — each group should leave at least one input free to optimize.
structure, outpath, store, and eval_timeout on PetrinautModelSpec are accepted but currently unused.
Not wired into monorepo tooling (no package.json), so it isn't picked up by turbo lint/test.

🐾 Next steps

TBD

🛡 What tests cover this?

None yet — no automated tests are included in this PR.

❓ How to test this?

Check out the branch.
Start the Petrinaut CLI server: petrinaut serve --model <path/to/model.json> --socket /tmp/petrinaut.sock (from libs/@hashintel/petrinaut-cli).
From libs/@hashintel/petrinaut-opt, run uv sync, then start the API: uv run uvicorn src.optimization_api:app --reload.
POST /init with an opt_spec + pn_spec body (see README) and grab the session_id.
curl -sN localhost:8000/optimize/<session_id>/stream/all and confirm one SSE frame per trial, ending in event: done. Repeat with /stream/best to confirm best-so-far frames.
Alternatively run the optimizer directly: uv run python -m src.petrinaut_optimizer.

📹 Demo

streaming_opt_results

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New standalone service with hard-coded socket path, in-memory sessions, and no tests; failures are mostly isolated per session but misconfiguration can wedge streams or prune all trials silently.

Overview
Introduces libs/@hashintel/petrinaut-opt, a new uv-managed Python package that black-box optimizes Petri-net runs by driving the existing petrinaut-cli Unix socket server and maximizing/minimizing a named metric via Optuna (TPE/random).

PetrinautModel opens /tmp/petrinaut.sock, sends JSON run requests (parameters, initialState, metrics, steps, dt, seed), and returns the objective. PetrinautOptimizer defines search BOUNDS, treats values in OptimizationSpec as fixed and omits as tunable, prunes failed trials, and runs studies in a background thread with stream_all / stream_best SSE (per-trial vs best-so-far; stream_best waits until at least one trial completes).

optimization_api.py adds a FastAPI app: POST /init creates session-scoped optimizers, GET /optimize/{session_id}/stream/* streams results (optional n_trials), DELETE, GET /status, plus per-session locks to block concurrent streams on the same session. Package includes README, pyproject.toml, and .gitignore; no automated tests in this PR.

Reviewed by Cursor Bugbot for commit b3ace77. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Error Error Jul 9, 2026 3:46pm
petrinaut Ready Ready Preview, Comment Jul 9, 2026 3:46pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 9, 2026 3:46pm

Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_optimizer.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_optimizer.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_client.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_client.py
@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Jul 9, 2026
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

break
finally:
stop_flag.set()
self.lock.release()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disconnect releases lock early

High Severity

When an SSE client disconnects, stream_all / stream_best exit their receive loop and the finally block releases the per-session lock while the daemon thread may still be inside study.optimize and PetrinautModel.run. A second stream on the same session_id can then start another optimization on the same Study and share one socket stream, causing overlapping Optuna runs and interleaved socket I/O.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b5e10c1. Configure here.

async def teardown(session_id: str):
if app.state.sessions.pop(session_id, None) is None:
raise HTTPException(404, "unknown session_id")
return {"status": "deleted"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DELETE ignores running optimization

Medium Severity

DELETE /optimize/{session_id} only removes the session from app.state.sessions and does not signal the optimizer to stop or wait for its background thread. An in-flight stream or Optuna worker keeps using the PetrinautModel socket until trials finish.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b5e10c1. Configure here.

Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_client.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_client.py Outdated
@YannisZa YannisZa changed the title Python Optimisation API for petrinaut-cli parameter and initial state optimisation FE-1162: Python Optimisation API for petrinaut-cli parameter and initial state optimisation Jul 9, 2026
@YannisZa YannisZa changed the title FE-1162: Python Optimisation API for petrinaut-cli parameter and initial state optimisation FE-1162: Python optimisation script (Optuna) driving the Petrinaut CLI Jul 9, 2026
Comment thread libs/@hashintel/petrinaut-opt/src/optimization_api.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_optimizer.py Outdated
Comment thread libs/@hashintel/petrinaut-opt/src/petrinaut_optimizer.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b3ace77. Configure here.

try:
self.study.optimize(
self.objective, n_trials=n_trials, callbacks=[callback]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same session adds more trials

Medium Severity

Each call to the SSE stream endpoints invokes study.optimize on the single Optuna study created at /init. A second stream on the same session_id runs another full n_trials batch and appends to the existing study instead of starting a fresh run of the configured trial count.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3ace77. Configure here.

yield f"data: {json.dumps(item)}\n\n"
if await request.is_disconnected():
stop_flag.set()
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disconnect checked after queue wait

Medium Severity

Client disconnect is only handled after await q.get() returns and a frame is yielded. While blocked waiting for the next trial, a disconnected client does not set stop_flag, so the background Optuna worker can start or finish additional expensive Petrinaut evaluations after the client has gone.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3ace77. Configure here.

@YannisZa

YannisZa commented Jul 9, 2026

Copy link
Copy Markdown
Author

Unresolved cursor comments are left unaddressed until we settle on how the Optimisation API will talk to the Node API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants