# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
.. _ref_proxy_solver_oco:

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:

   a. Create an ``OCO`` system (``node_types.OCO``) -- optiSLang Combined Optimization that
      automatically selects and combines multiple optimization algorithms (gradient-based,
      evolutionary, surrogate-based).
   b. 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).
   c. Set fast-running solver properties (``AutoSaveMode``, ``SolveTwice``, etc.).
   d. Add a ``ProxySolver`` node (``DesignFlow.RECEIVE_SEND``) inside the OCO system
      and configure batch size via ``MultiDesignLaunchNum``.
   e. Load 5 input parameters (X1..X5) and 1 response (Y) into the proxy solver via ``load()``.
   f. Register them as system-level parameters/responses and set bounds [-3.14, 3.14].
   g. 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:
#
# .. code:: python
#
#   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!
#
# .. image:: ../../_static/03_3_ProxySolverOCO.png
#  :width: 400
#  :alt: Result of script.
#
