Conversation
The two per-parameter fields the optimization engine reads for a dimension's unit and an objective's measurement address were declared in the library schema but never parsed by the AST analyzer nor forwarded to the frontend, so a node author's unit was invisible in the editor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…anvas The generator emits the library API exactly as documented: after workflow_builder.build(), every parameter marked optimizable in the editor is declared on its built node (optimizable, optimization_range, unit), build_spec() takes the algorithm from the node, the study's objectives are added with spec.add_objective() and optimize() runs the search. The optimization node is constructed and configured but neither added to the builder nor connected. Objectives live on the optimization node as a GUI-only data.study block whose node ids are resolved to the generated variable names at generation time, so renaming a node cannot break an address. Without the node the script is byte-identical to before; the fixed tail moved into NORMAL_TAIL. Two optimization nodes are refused with a 400. Toggling optimizable on a parameter marks it modified, so its unchanged default is also written into configure(); harmless and left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dropping a port-less node replaced its whole schema with a placeholder, so NW_Optimization landed on the canvas with no parameters and every parameter write to it failed. Only the ports are filled in now. The parameter type gains the unit, measures and allowed_values fields the backend sends, the dict form of optimization_range, and the GUI-side study block an NW_Optimization node carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Opening an NW_Optimization node shows the optimization study instead of the generic node panel: how to search (the node's own parameters), which parameters on the other nodes are explored, and the objectives. Explore rows are the target nodes' own optimizable / optimization_range / unit fields, written through the per-parameter endpoint and mirrored into the flow store at once so a later whole-node save cannot revert them. The add picker offers numeric scalars and the numeric keys of dict parameters, prefilled from the author's range, else constraints, else the value ± 50 %, and refuses a range outside the constraints. Objectives are kept on the optimization node as data.study.objectives, keyed by node id and output port, and saved with the whole-node PUT. Objectives a node author declared on a parameter are listed read-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A node with an explored parameter gets a tune badge, a node an objective measures gets a target badge, and the optimization node shows how many parameters and objectives the study holds. Display only, computed from the flow store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The study is declared as the library already reads it: explore flags on the parameters, objectives through spec.add_objective() held on the NW_Optimization node in the GUI, the algorithm on the node. The sections built in Phase 1 are marked as such. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
JSON cannot tell a Python 10.0 from 10, so inferring a whole-number axis from the default in the browser flagged float parameters. The panel now shows only constraints.integer; the engine still infers from the Python default when it builds the spec. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Address the unresolved None metadata handling and explore/objective range validation issues.
Pull request overview
Adds Phase 1 GUI support for configuring optimization studies on NW_Optimization, including study editing, badges, persistence, metadata extraction, and optimization-aware code generation.
Changes:
- Added exploration and objective configuration interfaces.
- Added optimization-aware backend generation and validation tests.
- Preserved port-less node parameters and forwarded
unit/measuresmetadata. - Added optimization documentation and handoff notes.
File summaries
| File | Summary |
|---|---|
gui/workflow_frontend/src/views/home/WorkflowCanvas.tsx |
Preserves parameters for port-less nodes. |
gui/workflow_frontend/src/views/home/utils/studyAddress.ts |
Provides study address and range helpers. |
gui/workflow_frontend/src/views/home/type.ts |
Defines study and optimization metadata types. |
gui/workflow_frontend/src/views/home/homeView.tsx |
Opens the optimization study modal. |
gui/workflow_frontend/src/views/home/components/optimization/studyApi.ts |
Persists study parameter fields. |
gui/workflow_frontend/src/views/home/components/optimization/OptimizationStudyModal.tsx |
Implements study configuration. |
gui/workflow_frontend/src/views/home/components/optimization/ObjectivesSection.tsx |
Manages objectives; range validation needs correction. |
gui/workflow_frontend/src/views/home/components/optimization/ExploreSection.tsx |
Manages explored parameters; edit validation needs correction. |
gui/workflow_frontend/src/views/home/components/optimization/draftInputs.tsx |
Provides draft input handling. |
gui/workflow_frontend/src/views/home/components/calculationNode.tsx |
Displays optimization badges. |
gui/workflow_backend/django-project/tests/test_code_generation_optimization.py |
Tests optimization code generation. |
gui/workflow_backend/django-project/app/workflow/views.py |
Returns generation errors as HTTP 400. |
gui/workflow_backend/django-project/app/workflow/code_generation_service.py |
Generates optimization scripts. |
gui/workflow_backend/django-project/app/box/services/python_analyzer.py |
Extracts metadata; preserves explicit None values correctly. |
gui/workflow_backend/django-project/app/box/models.py |
Forwards extracted metadata. |
docs/OPTIMIZATION.md |
Updates optimization documentation. |
docs/OPTIMIZATION_GUI_HANDOFF.md |
Documents the settled Phase 1 design. |
Review details
Suppressed comments (5)
gui/workflow_backend/django-project/app/box/services/python_analyzer.py:625
- This dict-style extraction has the same optional-value bug as the call-style path: an explicit
measures=Noneis converted to the string"None"by_extract_string_value. That invalid address is then forwarded to the frontend and objective discovery; preserve a null/empty value instead of stringifyingNone.
param_info["unit"] = self._extract_string_value(value)
elif key_name == "measures":
param_info["measures"] = self._extract_string_value(value)
gui/workflow_backend/django-project/app/box/services/python_analyzer.py:479
ParameterDefinition.measuresis optional, but_extract_string_value(ast.Constant(None))returns the literal string"None". A node that explicitly declaresmeasures=Nonewill therefore be forwarded as an invalid measurement address, shown asNonein the study, andbuild_spec()will try to resolve it instead of treating the objective as having no measures.
elif keyword.arg == "unit":
param_info["unit"] = self._extract_string_value(keyword.value)
elif keyword.arg == "measures":
param_info["measures"] = self._extract_string_value(keyword.value)
gui/workflow_frontend/src/views/home/components/optimization/ExploreSection.tsx:46
- Edits to an existing explore row bypass the validation used by the add form: any
low/highpair, includinglow >= highor values outside the parameter constraints, is sent to the backend. The backend does not validate these fields, so Generate can emit a spec that the optimization engine rejects; validate against the row's parameter bounds and ordering before callingsetParamField(and surface the error) just as the add path does.
const commitRange = async (row: ExploreRow, low: number | undefined, high: number | undefined) => {
if (low === undefined || high === undefined) return;
if (row.key) {
const dict = rangeDict(nodeById(row.nodeId), row.param);
dict[row.key] = [low, high];
await setParamField(row.nodeId, row.param, "optimization_range", dict);
} else {
await setParamField(row.nodeId, row.param, "optimization_range", [low, high]);
}
gui/workflow_frontend/src/views/home/components/optimization/ObjectivesSection.tsx:53
- The engine explicitly permits a zero-width
in_rangetarget (low == high) as a point target, but this validation rejects it and prevents the GUI from creating the same valid objective. Uselow > highhere (and update the accompanying message) so the panel matchesOptimizationSpec.validate().
const badRange = needsRange && (low === undefined || high === undefined || low >= high);
const canAdd = !!node && !!port && !!effectiveName && !duplicate && !badRange;
gui/workflow_frontend/src/views/home/components/optimization/ObjectivesSection.tsx:129
- The row editors can persist an invalid
in_rangeobjective: changing either bound callsupdateeven when the resulting range is missing or inverted. Since_objective_linesemits the stored values without validation, Generate can producespec.add_objectivewith an invalid target and fail at runtime; reject or disable these updates untillow < high.
<Td>
<DraftNumberInput value={o.low ?? undefined} allowEmpty={!inRange} isInvalid={bad} onCommit={(v) => update(i, { low: v ?? null })} />
</Td>
<Td>
<DraftNumberInput value={o.high ?? undefined} allowEmpty={!inRange} isInvalid={bad} onCommit={(v) => update(i, { high: v ?? null })} />
- Files reviewed: 17/17 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Stacked on #95. The library's code representation is unchanged: the generated script uses the optimization API exactly as
notebooks/generated_optimization_example.pyshows it. On the GUI the study is aggregated on theNW_Optimizationnode, as agreed with @carlosengutierrez.What a user gets
NW_Optimizationnode shows the study instead of the generic node panel: how to search (its own parameters,algorithmas a dropdown, a budget line), which parameters on the other nodes are explored, and the objectives.optimizable/optimization_range/unit), so the generic node panel and the study always agree. The add picker offers numeric scalars and the numeric keys of dict parameters, prefills the range from the author'soptimization_range, elseconstraints.min/max, else the value ± 50 %, and refuses a range outside the constraints.data.study.objectives(node → output port → optional key, goal, target range, unit). They are keyed by node id, so renaming a node cannot break an address. Objectives a node author declared on a parameter (is_objective) are listed read-only;build_spec()discovers them itself.tuneon a node with an explored parameter,targeton a node an objective measures,study N/Mon the optimization node.NW_Optimizationnode the script becomes a search; with none it is byte-identical to before; with two the request is refused with a clear message.Generated tail (from a real canvas)
The optimization node is constructed and configured but neither added to the builder nor connected.
Also in this PR
unitandmeasuresare now extracted fromParameterDefinitionand forwarded to the frontend (re-sync the palette once).NW_Optimizationused to land with none).docs/OPTIMIZATION_GUI_HANDOFF.mdrecords the settled design.Not in this PR (Phase 2)
Run/Results tabs over the ledger, the
control.jsonendpoint, a kernel interrupt, and Adopt best. Two pre-existing gaps matter for long runs: aborting the SSE stream leaves the kernel running, andEXECUTE_IDLE_TIMEOUTis 600 s. The nest image must be rebuilt withoptuna/cmaesbefore a generated search can run.Verification
tests/test_code_generation_optimization.py(15 tests): detection, skip of the optimization node inadd_node/connect, explore and objective emission, the template tail being unchanged in normal mode,ast.parseof the optimization tail, idempotent regeneration in both directions, the 400 on two nodes. All pass in the backend image.tsc -bandvite buildclean; ESLint clean on the new files.NW_Optimization,NW_IClamp,NW_Population,NW_Analysis; addedamp_naandnest_params.tau_mto explore andfiring_rate_hz.excas an objective; badges appeared; the study survived a page reload; Generate produced the tail above; after deleting the optimization node the script returned to the normal tail.Replaces #98, which changed the library and is closed.
🤖 Generated with Claude Code