Skip to content

Optimization study GUI on the NW_Optimization node (Phase 1) - #99

Open
IzBrain67 wants to merge 7 commits into
feat/schema-improvementsfrom
feat/optimization-gui
Open

IzBrain67 wants to merge 7 commits into
feat/schema-improvementsfrom
feat/optimization-gui

Conversation

@IzBrain67

Copy link
Copy Markdown
Collaborator

Stacked on #95. The library's code representation is unchanged: the generated script uses the optimization API exactly as notebooks/generated_optimization_example.py shows it. On the GUI the study is aggregated on the NW_Optimization node, as agreed with @carlosengutierrez.

What a user gets

  • Opening an NW_Optimization node shows the study instead of the generic node panel: how to search (its own parameters, algorithm as a dropdown, a budget line), which parameters on the other nodes are explored, and the objectives.
  • Explore rows are the target nodes' own fields (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's optimization_range, else constraints.min/max, else the value ± 50 %, and refuses a range outside the constraints.
  • Objectives live on the optimization node as 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.
  • Canvas badges: tune on a node with an explored parameter, target on a node an objective measures, study N/M on the optimization node.
  • One Generate button. With one NW_Optimization node 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)

    workflow = workflow_builder.build()

    # Print workflow information
    print(workflow)

    # Parameters marked optimizable in the editor
    instance_NW_IClamp_002.NODE_DEFINITION.parameters["amp_na"].optimizable = True
    instance_NW_IClamp_002.NODE_DEFINITION.parameters["amp_na"].optimization_range = [100, 1000]
    instance_NW_IClamp_002.NODE_DEFINITION.parameters["amp_na"].unit = 'nA'

    instance_NW_Population_003.NODE_DEFINITION.parameters["nest_params"].optimizable = True
    instance_NW_Population_003.NODE_DEFINITION.parameters["nest_params"].optimization_range = {'tau_m': [5, 15]}

    # Execute optimization
    print("\nOptimizing workflow...")
    spec = build_spec(workflow, instance_NW_Optimization_001.algorithm_config())

    # Objectives declared by the study
    spec.add_objective(
        name='NW_Analysis_firing_rate_hz_exc',
        measures='instance_NW_Analysis_004.firing_rate_hz.exc',
        low=40,
        high=50,
        unit='Hz'
    )

    result = optimize(workflow, spec=spec, results_path=instance_NW_Optimization_001.results_path())
    ...

The optimization node is constructed and configured but neither added to the builder nor connected.

Also in this PR

  • unit and measures are now extracted from ParameterDefinition and forwarded to the frontend (re-sync the palette once).
  • Dropping a port-less node no longer wipes its parameters (NW_Optimization used to land with none).
  • docs/OPTIMIZATION_GUI_HANDOFF.md records the settled design.

Not in this PR (Phase 2)

Run/Results tabs over the ledger, the control.json endpoint, a kernel interrupt, and Adopt best. Two pre-existing gaps matter for long runs: aborting the SSE stream leaves the kernel running, and EXECUTE_IDLE_TIMEOUT is 600 s. The nest image must be rebuilt with optuna/cmaes before a generated search can run.

Verification

  • tests/test_code_generation_optimization.py (15 tests): detection, skip of the optimization node in add_node/connect, explore and objective emission, the template tail being unchanged in normal mode, ast.parse of the optimization tail, idempotent regeneration in both directions, the 400 on two nodes. All pass in the backend image.
  • Frontend: tsc -b and vite build clean; ESLint clean on the new files.
  • In the browser: dropped NW_Optimization, NW_IClamp, NW_Population, NW_Analysis; added amp_na and nest_params.tau_m to explore and firing_rate_hz.exc as 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

IzBrain67 and others added 7 commits September 17, 2026 07:50
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>

Copilot AI 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.

🔵 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/measures metadata.
  • 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=None is 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 stringifying None.
                    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.measures is optional, but _extract_string_value(ast.Constant(None)) returns the literal string "None". A node that explicitly declares measures=None will therefore be forwarded as an invalid measurement address, shown as None in the study, and build_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/high pair, including low >= high or 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 calling setParamField (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_range target (low == high) as a point target, but this validation rejects it and prevents the GUI from creating the same valid objective. Use low > high here (and update the accompanying message) so the panel matches OptimizationSpec.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_range objective: changing either bound calls update even when the resulting range is missing or inverted. Since _objective_lines emits the stored values without validation, Generate can produce spec.add_objective with an invalid target and fail at runtime; reject or disable these updates until low < 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants