Proxy solver with OCO#

This example demonstrates how to obtain designs from a parametric system and process them externally.

It creates a proxy solver node inside an OCO parametric system, modifies the maximum number of designs in the OCO settings, and solves its designs externally. This is a unified approach for “optiSLang inside” solutions.

Workflow overview (mapped to code sections below):

  1. “Perform required imports” – Import pyOptislang classes for workflow creation.

  2. “Create solver” – Define a Python calculator function that serves as the external solver. This function receives design dicts with parameter values and returns response values.

  3. “Create optiSLang instance” – Discover an optiSLang >= 25.1 installation and start a headless optiSLang server.

  4. “Create workflow” – Build the parametric workflow:

    1. Create an OCO system (node_types.OCO) – optiSLang Combined Optimization that automatically selects and combines multiple optimization algorithms (gradient-based, evolutionary, surrogate-based).

    2. Read and modify algorithm settings via get_property("Settings"): the OCO settings use a {"sequence": [{"First": key, "Second": value}, ...]} format. This example modifies "Maximum number of samples" to 150 (default 200).

    3. Set fast-running solver properties (AutoSaveMode, SolveTwice, etc.).

    4. Add a ProxySolver node (DesignFlow.RECEIVE_SEND) inside the OCO system and configure batch size via MultiDesignLaunchNum.

    5. Load 5 input parameters (X1..X5) and 1 response (Y) into the proxy solver via load().

    6. Register them as system-level parameters/responses and set bounds [-3.14, 3.14].

    7. Add a minimization criterion on Y.

  5. “Run workflow” – Start the project in non-blocking mode and loop externally: poll proxy_solver.get_designs(), compute responses via the calculator, and return results via proxy_solver.set_designs() until the system finishes.

  6. “Stop and cancel project” – Dispose of the optiSLang instance.

Key APIs used:

  • node_types.OCO – optiSLang Combined Optimization algorithm

  • get_property("Settings") / set_property("Settings", ...) – read/write OCO-specific settings; the dict uses {"sequence": [{"First": name, "Second": value}, ...]} format

  • node_types.ProxySolver with DesignFlow.RECEIVE_SEND – external solver integration

  • proxy_solver.get_designs() / proxy_solver.set_designs() – design exchange loop

  • register_locations_as_parameter() / register_locations_as_response() – register solver slots as system-level parameters and responses

Perform required imports#

Import the pyOptislang core classes: Optislang for server management, node_types for algorithm and solver type constants, DesignFlow/ParametricSystem/ProxySolverNode for workflow construction, and parametric classes for defining parameters, criteria, and bounds.

import time

from ansys.optislang.core import Optislang
import ansys.optislang.core.node_types as node_types
from ansys.optislang.core.nodes import DesignFlow, ParametricSystem, ProxySolverNode
from ansys.optislang.core.project_parametric import (
    ComparisonType,
    ObjectiveCriterion,
    OptimizationParameter,
)
from ansys.optislang.core.utils import find_all_osl_exec

Create solver#

Define the external solver functions that will evaluate designs outside optiSLang. calculator() computes response Y from 5 input parameters using a nonlinear formula. calculate() processes a batch of design dicts received from the proxy solver: each design contains hid (design ID) and parameters (list of name/value pairs). It returns a list of result dicts with hid and responses (list of name/value pairs).

def calculator(hid, X1, X2, X3, X4, X5):
    from math import sin

    Y = 0.5 * X1 + X2 + 0.5 * X1 * X2 + 5 * sin(X3) + 0.2 * X4 + 0.1 * X5
    return Y


def calculate(designs):
    result_design_list = []
    print(f"Calculate {len(designs)} designs")
    for design in designs:
        hid = design["hid"]
        parameters = design["parameters"]
        X1 = 0.0
        X2 = 0.0
        X3 = 0.0
        X4 = 0.0
        X5 = 0.0
        for parameter in parameters:
            if parameter["name"] == "X1":
                X1 = parameter["value"]
            elif parameter["name"] == "X2":
                X2 = parameter["value"]
            elif parameter["name"] == "X3":
                X3 = parameter["value"]
            elif parameter["name"] == "X4":
                X4 = parameter["value"]
            elif parameter["name"] == "X5":
                X5 = parameter["value"]
        Y = calculator(hid, X1, X2, X3, X4, X5)

        result_design = {}
        result_design["hid"] = hid
        responses = [{"name": "Y", "value": Y}]
        result_design["responses"] = responses
        result_design_list.append(result_design)

    print(f"Return {len(result_design_list)} designs")
    return result_design_list

Create optiSLang instance#

Discover available optiSLang installations using find_all_osl_exec(). The ProxySolver node requires optiSLang >= 25R1 (version code 251). Optislang(executable=...) starts a headless optiSLang server process and establishes a TCP connection for remote control.

available_optislang_executables = find_all_osl_exec()
if not available_optislang_executables:
    raise KeyError("No optiSLang installation was found, please specify path manually.")
version, executables = available_optislang_executables.popitem(last=False)
if not version >= 251:
    raise KeyError("OptiSLang installation >= 25R1 wasn't found, please specify path manually.")

osl = Optislang(executable=executables[0])

print(f"Using optiSLang version {osl.osl_version_string}")

Create workflow#

Build the complete parametric workflow. The root system is the top-level container for all nodes in an optiSLang project.

root_system = osl.application.project.root_system

# **Step 4a: Create the OCO algorithm system.**
# ``node_types.OCO`` (optiSLang Combined Optimization) automatically selects and combines
# multiple optimization algorithms -- gradient-based (NLPQL, MISQP), evolutionary (EA, PSO),
# surrogate-based (ARSM, Kriging), and others -- to find optimal solutions efficiently.

algorithm_system: ParametricSystem = root_system.create_node(type_=node_types.OCO, name="OCO")

# **Step 4b: Read and modify algorithm settings.**
# ``get_property("Settings")`` returns the OCO-specific settings dict.
# Unlike Sensitivity/AMOP, OCO settings use a list-of-pairs format:
# ``{"sequence": [{"First": key_name, "Second": value}, ...]}``
# This example modifies ``"Maximum number of samples"`` from the default 200 to 150,
# which controls how many total designs OCO will evaluate.
# The loop searches for the matching entry by name and updates its value.

max_num_designs = 150

oco_settings = algorithm_system.get_property("Settings")
max_num_samples_entry_found = False
for entry in oco_settings["sequence"]:
    if entry["First"] == "Maximum number of samples":
        entry["Second"] = max_num_designs
        max_num_samples_entry_found = True
        break
if not max_num_samples_entry_found:
    raise KeyError(
        'Could not apply OCO setting "Maximum number of samples": '
        'the entry was not found in Settings["sequence"].'
    )
algorithm_system.set_property("Settings", oco_settings)

# **Step 4c: Set fast-running solver properties.**
# These reduce I/O overhead for fast external solvers:
# - ``AutoSaveMode``: disable auto-saving to avoid filesystem delays.
# - ``SolveTwice``: re-evaluate the reference design for verification.
# - ``UpdateResultFile``: skip writing intermediate result files.
# Note: OCO does not support ``WriteDesignStartSetFlag`` (unlike Sensitivity).

algorithm_system.set_property("AutoSaveMode", "no_auto_save")
algorithm_system.set_property("SolveTwice", True)
algorithm_system.set_property("UpdateResultFile", "never")

# **Step 4d: Add the Proxy Solver node.**
# The ``ProxySolver`` with ``DesignFlow.RECEIVE_SEND`` acts as a bridge: optiSLang sends
# designs to it, and the external Python code retrieves them via ``get_designs()``,
# evaluates them, and returns results via ``set_designs()``.
# ``MultiDesignLaunchNum`` controls the batch size (99 = up to 99 designs per batch;
# set to -1 to receive all pending designs at once).

proxy_solver: ProxySolverNode = algorithm_system.create_node(
    type_=node_types.ProxySolver, name="Calculator", design_flow=DesignFlow.RECEIVE_SEND
)

multi_design_launch_num = 99  # set -1 to solve all designs simultaneously
proxy_solver.set_property("MultiDesignLaunchNum", multi_design_launch_num)
proxy_solver.set_property("ForwardHPCLicenseContextEnvironment", True)

# **Step 4e: Load parameters and responses into the proxy solver.**
# Define 5 input parameters (X1..X5) with reference value 1.0 and 1 output response (Y)
# with reference value 3.0. The ``load()`` call configures the proxy solver's interface
# so optiSLang knows which values to send and expect back.

load_json = {}
load_json["parameters"] = []
load_json["responses"] = []

for i in range(1, 6):
    parameter = {"dir": {"value": "input"}, "name": f"X{i}", "value": 1.0}
    load_json["parameters"].append(parameter)

response = {"dir": {"value": "output"}, "name": "Y", "value": 3.0}
load_json["responses"].append(response)

proxy_solver.load(args=load_json)

# **Step 4f: Register parameters/responses and set bounds.**
# ``register_locations_as_parameter()`` promotes the proxy solver's input slots to
# system-level parameters visible to the OCO algorithm.
# ``register_locations_as_response()`` does the same for outputs.
# Then modify each parameter to an ``OptimizationParameter`` with bounds [-3.14, 3.14].

proxy_solver.register_locations_as_parameter()
proxy_solver.register_locations_as_response()

for i in range(1, 6):
    algorithm_system.parameter_manager.modify_parameter(
        OptimizationParameter(name=f"X{i}", reference_value=1.0, range=(-3.14, 3.14))
    )

# **Step 4g: Add optimization criterion.**
# Add a minimization objective on the response Y. OCO will use its combined
# optimization strategy to find the parameter combination that minimizes Y.

algorithm_system.criteria_manager.add_criterion(
    ObjectiveCriterion(name="obj", expression="Y", criterion=ComparisonType.MIN)
)

Optionally save project#

If you want to save the project to some desired location, uncomment and edit these lines:

dir_path = Path(r"<insert-desired-location>")
project_name = "proxy_solver_oco_workflow.opf"
osl.application.save_as(dir_path / project_name)

Run workflow#

Step 5: Execute the workflow with the external proxy solver loop. start(wait_for_finished=False) launches the optiSLang project execution in the background. The while-loop then acts as the external solver: it polls get_designs() to receive pending design batches from optiSLang, evaluates them with the calculate() function, and returns the computed responses via set_designs(). The loop continues until get_status() reports "Processing done".

osl.application.project.start(wait_for_finished=False)

while not osl.project.root_system.get_status() == "Processing done":
    design_list = proxy_solver.get_designs()
    if len(design_list):
        responses_dict = calculate(design_list)
        proxy_solver.set_designs(responses_dict)
    time.sleep(0.1)

print("Solved Successfully!")

Stop and cancel project#

Step 6: Dispose of the optiSLang instance. dispose() stops the optiSLang server process and cleans up resources.

osl.dispose()

View generated workflow#

This image shows the generated workflow. However, it is important to note, that this workflow is only usable through pyoptislang and cannot be used interactively!

Result of script.

Gallery generated by Sphinx-Gallery