Proxy solver with Sensitivity#

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

It creates a proxy solver node inside a Sensitivity parametric system 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 a Sensitivity system (node_types.Sensitivity) – Latin Hypercube sampling.

    2. Read and modify algorithm settings via get_property("AlgorithmSettings"): set num_discretization to 2000 sampling points.

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

    4. Add a ProxySolver node (DesignFlow.RECEIVE_SEND) inside the Sensitivity 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.Sensitivity – Sensitivity analysis algorithm (Latin Hypercube sampling)

  • get_property("AlgorithmSettings") / set_property("AlgorithmSettings", ...) – read/write algorithm-specific settings (e.g. num_discretization)

  • 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()
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 Sensitivity algorithm system.**
# ``node_types.Sensitivity`` performs a design of experiments using Latin Hypercube sampling
# to explore the parameter space and compute sensitivity indices (e.g. CoP values).

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

# **Step 4b: Read and modify algorithm settings.**
# ``get_property("AlgorithmSettings")`` returns the Sensitivity-specific settings dict.
# ``num_discretization`` controls the number of sampling points (default is lower; set to 2000
# for a more thorough exploration). The modified dict is written back with ``set_property()``.

num_discretization = 2000

algorithm_settings = algorithm_system.get_property("AlgorithmSettings")
algorithm_settings["num_discretization"] = num_discretization
algorithm_system.set_property("AlgorithmSettings", algorithm_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.
# - ``WriteDesignStartSetFlag``: skip writing start-set markers.

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

# **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 Sensitivity 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. The Sensitivity algorithm will compute
# sensitivity metrics for this criterion across the sampled design space.

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