Proxy solver with AMOP#

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

It creates a proxy solver node inside an AMOP parametric system, modifies the maximum number of designs in the AMOP 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 AMOP system (node_types.AMOP) – Adaptive Metamodel-based Optimization Process that iteratively builds surrogate models and refines the design space.

    2. Read and modify algorithm settings via get_property("AMopSettings"): set num_designs_max to 150 (maximum number of designs to evaluate).

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

    4. Add a ProxySolver node (DesignFlow.RECEIVE_SEND) inside the AMOP 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.AMOP – Adaptive Metamodel-based Optimization Process algorithm

  • get_property("AMopSettings") / set_property("AMopSettings", ...) – read/write AMOP-specific settings (e.g. num_designs_max, num_discretization_initial)

  • 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 executables were 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 AMOP algorithm system.**
# ``node_types.AMOP`` (Adaptive Metamodel-based Optimization Process) iteratively builds
# and refines surrogate models (MOP) while exploring the design space, then runs optimization
# on the surrogate to find promising regions and validates them with the real solver.

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

# **Step 4b: Read and modify algorithm settings.**
# ``get_property("AMopSettings")`` returns the AMOP-specific settings dict.
# ``num_designs_max`` controls the maximum total number of designs AMOP will evaluate
# across all iterations (default is 300; set to 150 here).
# The modified dict is written back with ``set_property()``.

max_num_designs = 150

amop_settings = algorithm_system.get_property("AMopSettings")
amop_settings["num_designs_max"] = max_num_designs
algorithm_system.set_property("AMopSettings", amop_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.

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 AMOP 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. AMOP will build surrogate models for Y
# and optimize on them to find the minimum.

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_amop_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