Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for non_parametric optimization #100

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/ansys/simai/core/api/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ class OptimizationClientMixin(ApiClientMixin):
def define_optimization(self, workspace_id: str, optimization_parameters: Dict):
return self._post(f"workspaces/{workspace_id}/optimizations", json=optimization_parameters)

def run_optimization_trial(
self, optimization_id: str, geometry_id: str, geometry_parameters: Dict
):
def run_optimization_trial(self, optimization_id: str, parameters: Dict):
return self._post(
f"optimizations/{optimization_id}/trial-runs/{geometry_id}",
json=geometry_parameters,
f"optimizations/{optimization_id}/trial-runs",
json=parameters,
)

def get_optimization(self, optimization_id: str):
Expand Down
160 changes: 128 additions & 32 deletions src/ansys/simai/core/data/optimizations.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I could not wrap my head around the interaction of the different components I made this:

sequenceDiagram
    participant User
    participant OptDir as OptimizationDirectory
    participant TrialDir as OptimizationTrialRunDirectory

    User ->> OptDir: run_parametric
    activate User
    activate OptDir
    create participant Opti as Optimization
    OptDir -x Opti: creates


    loop Until optimization complete
        OptDir->>Opti: _run_iteration
        activate Opti
        Opti->>TrialDir: _run_iteration
        activate TrialDir
        create participant Trial as OptimizationTrialRun
        TrialDir-xTrial: Creates
        activate Trial
        TrialDir-->>Opti: returns OptimizationTrialRun
        deactivate TrialDir
        Opti-->>OptDir: returns OptimizationTrialRun
        deactivate Opti
        OptDir->>Trial: Get iteration results
        deactivate Trial
    end
    deactivate OptDir

    OptDir-->>User: result (List[Dict])
    deactivate User
Loading

Conclusions:

  • Some objects are not meant to be handled by end users (OptimizationTrialRun and OptimizationTrialRunDirectory so should be prefixed by "_")
  • The relationships between objects must be better defined in the code, even in the definition order, define the children first: OptimizationTrialRun then OptimizationTrialRunDirectory then Optimization then OptimizationDirectory
  • Maybe _run_iteration should only be defined on the Optimization ?
  • I don't understand what OptimizationTrialRunDirectory.get could be used for

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import logging
from inspect import signature
from typing import Callable, Dict, List, Optional, Tuple
from typing import Callable, Dict, List, Literal, Optional, Tuple

from tqdm import tqdm
from wakepy import keep
Expand All @@ -33,6 +33,7 @@
Identifiable,
NamedFile,
get_id_from_identifiable,
get_object_from_identifiable,
)
from ansys.simai.core.data.workspaces import Workspace
from ansys.simai.core.errors import InvalidArguments
Expand All @@ -43,12 +44,8 @@
class Optimization(ComputableDataModel):
"""Provides the local representation of an optimization definition object."""

def _try_geometry(
self, geometry: Identifiable[Geometry], geometry_parameters: Dict
) -> "OptimizationTrialRun":
return self._client._optimization_trial_run_directory._try_geometry(
self.id, geometry, geometry_parameters
)
def _run_iteration(self, parameters: Dict) -> "OptimizationTrialRun":
return self._client._optimization_trial_run_directory._run_iteration(self.id, parameters)


class OptimizationTrialRun(ComputableDataModel):
Expand Down Expand Up @@ -86,7 +83,7 @@
"""
return self._model_from(self._client._api.get_optimization(optimization_id))

def run(
def run_parametric(
self,
geometry_generation_fn: Callable[..., NamedFile],
geometry_parameters: Dict[str, Tuple[float, float]],
Expand All @@ -98,7 +95,7 @@
show_progress: bool = False,
workspace: Optional[Identifiable[Workspace]] = None,
) -> List[Dict]:
"""Run an optimization process.
"""Run an optimization loop with client-side parametric geometry generation.

Args:
geometry_generation_fn: Function to call to generate a new geometry
Expand All @@ -107,6 +104,7 @@
geometry_parameters: Name of the geometry parameters and their bounds or possible values (choices).
boundary_conditions: Values of the boundary conditions to perform the optimization at.
The values should map to existing boundary conditions in your project/workspace.
n_iters: Number of iterations of the optimization loop.
minimize: List of global coefficients to minimize.
The global coefficients should map to existing coefficients in your project/workspace.
maximize: List of global coefficients to maximize.
Expand All @@ -119,7 +117,6 @@
- ``x`` is a float bound.
- The comparison operator is ``>=`` or ``<=``.

n_iters: Number of iterations of the optimization loop.
show_progress: Whether to print progress on stdout.
workspace: Workspace to run the optimization in. If a workspace is
not specified, the default is the configured workspace.
Expand Down Expand Up @@ -162,22 +159,18 @@
print(results)
"""
workspace_id = get_id_from_identifiable(workspace, True, self._client._current_workspace)
_check_geometry_generation_fn_signature(geometry_generation_fn, geometry_parameters)
if not minimize and not maximize:
raise InvalidArguments("No global coefficient to optimize.")
objective = {}
if minimize:
for global_coefficient in minimize:
objective[global_coefficient] = {"minimize": True}
if maximize:
for global_coefficient in maximize:
objective[global_coefficient] = {"minimize": False}
_validate_geometry_parameters(geometry_parameters)
_validate_geometry_generation_fn_signature(geometry_generation_fn, geometry_parameters)
objective = _build_objective(minimize, maximize)
optimization_parameters = {
"boundary_conditions": boundary_conditions,
"geometry_parameters": geometry_parameters,
"n_iters": n_iters,
"objective": objective,
"type": "parametric",
"outcome_constraints": outcome_constraints or [],
"geometry_generation": {
"geometry_parameters": geometry_parameters,
},
}
with tqdm(total=n_iters, disable=not show_progress) as progress_bar:
progress_bar.set_description("Creating optimization definition.")
Expand All @@ -204,7 +197,9 @@
)
logger.debug("Running trial.")
progress_bar.set_description("Running trial.")
trial_run = optimization._try_geometry(geometry, geometry_parameters)
trial_run = optimization._run_iteration(
{"geometry": geometry.id, "geometry_parameters": geometry_parameters}
)
trial_run.wait()
iteration_result = {
"parameters": geometry_parameters,
Expand All @@ -222,15 +217,122 @@
progress_bar.set_description("Optimization complete.")
return iterations_results

def run_non_parametric(
self,
geometry: Identifiable[Geometry],
bounding_boxes: List[List[float]],
symmetries: List[Literal["x", "y", "z", "X", "Y", "Z"]],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the value of typing for both upper and lower case is not worth it compared to "Hugh ? Why am I seeing double ?"

boundary_conditions: Dict[str, float],
n_iters: int,
minimize: Optional[List[str]] = None,
maximize: Optional[List[str]] = None,
show_progress: bool = False,
):
"""Run an optimization loop with server-side geometry generation using automorphism.

Args:
geometry: The base geometry on which to perform the automorphism. The optimization will
run in the same workspace as the geometry.
bounding_boxes: list of the bounds of the different boxes that will define the locations
of the geometry to optimize.
The format is [
[box1_xmin, box1_xmax, box1_ymin, box1_ymax, box1_zmin, box1_zmax],
[box2_xmin, box2_xmax, box2_ymin, box2_ymax, box2_zmin, box2_zmax],
...
]
symmetries: list of symmetry axes, axes being x, y or z
boundary_conditions: Values of the boundary conditions to perform the optimization at.
The values should map to existing boundary conditions in your project/workspace.
n_iters: Number of iterations of the optimization loop.
minimize: List of global coefficients to minimize.
The global coefficients should map to existing coefficients in your project/workspace.
maximize: List of global coefficients to maximize.
The global coefficients should map to existing coefficients in your project/workspace.
show_progress: Whether to print progress on stdout.
workspace: Workspace to run the optimization in. If a workspace is
not specified, the default is the configured workspace.
"""
geometry = get_object_from_identifiable(geometry, self._client._geometry_directory)
objective = _build_objective(minimize, maximize)
optimization_parameters = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since optimization parameters is used both in run_parametric and run_non_parametric with small differences, it might deserve to have a function or class for its construction, according to parametric or non-parametric. Maye it would save some a few lines of code too 😄

"boundary_conditions": boundary_conditions,
"n_iters": n_iters,
"objective": objective,
"type": "non_parametric",
"geometry_generation": {
"geometry": geometry.id,
"box_bounds_list": bounding_boxes,
"symmetries": symmetries,
},
}
with tqdm(total=n_iters, disable=not show_progress) as progress_bar:
progress_bar.set_description("Creating optimization definition.")
optimization = self._model_from(
self._client._api.define_optimization(
geometry._fields["workspace_id"], optimization_parameters
)
)
optimization.wait()
logger.debug("Optimization defined. Starting optimization loop.")
iterations_results: List[Dict] = []
with keep.running(on_fail="warn"):
for _ in range(n_iters):
logger.debug("Running iteration")
progress_bar.set_description("Running iteration")
trial_run = optimization._run_iteration({})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This {} arg could really use a validation with a raise InvalidArguments

trial_run.wait()
iteration_result = {
"objective": trial_run.fields["outcome_values"],
}
progress_bar.set_postfix(**iteration_result)
if trial_run.fields.get("is_feasible", True):
iterations_results.append(iteration_result)
else:
logger.debug("Trial run results did not match constraints. Skipping.")

Check warning on line 291 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L291

Added line #L291 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is something not important for the user, and it doesn't need to be notified.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is why it's in debug level. Most users won't ever see log from the SDK, you have to specifically enable them, even more so when it's debug.

logger.debug("Trial complete.")
progress_bar.update(1)
logger.debug("Optimization complete.")
progress_bar.set_description("Optimization complete.")
return iterations_results


def _check_geometry_generation_fn_signature(geometry_generation_fn, geometry_parameters):
def _validate_geometry_parameters(params: Dict):
if not isinstance(params, Dict):
raise InvalidArguments("geometry_parameters: must be a dict.")

Check warning on line 301 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L301

Added line #L301 was not covered by tests
tmpbeing marked this conversation as resolved.
Show resolved Hide resolved
if not params:
raise InvalidArguments("geometry_parameters: must not be empty.")

Check warning on line 303 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L303

Added line #L303 was not covered by tests
tmpbeing marked this conversation as resolved.
Show resolved Hide resolved
for key, value in params.items():
bounds = value.get("bounds")
choices = value.get("choices")
if not bounds and not choices:
raise InvalidArguments(f"geometry_parameters: no bounds or choices specified for {key}")

Check warning on line 308 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L308

Added line #L308 was not covered by tests
tmpbeing marked this conversation as resolved.
Show resolved Hide resolved
if bounds and choices:
raise InvalidArguments(

Check warning on line 310 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L310

Added line #L310 was not covered by tests
f"geometry_parameters: only one of bounds or choices must be specified for {key}"
tmpbeing marked this conversation as resolved.
Show resolved Hide resolved
)


def _validate_geometry_generation_fn_signature(geometry_generation_fn, geometry_parameters):
geometry_generation_fn_args = signature(geometry_generation_fn).parameters
if geometry_generation_fn_args.keys() != geometry_parameters.keys():
raise InvalidArguments(
f"geometry_generation_fn requires the following signature: {list(geometry_parameters.keys())}, but got: {list(geometry_generation_fn_args.keys())}"
)


def _build_objective(minimize: List[str], maximize: List[str]) -> Dict:
if not minimize and not maximize:
raise InvalidArguments("No global coefficient to optimize.")

Check warning on line 325 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L325

Added line #L325 was not covered by tests
objective = {}
if minimize:
for global_coefficient in minimize:
objective[global_coefficient] = {"minimize": True}
if maximize:
for global_coefficient in maximize:
objective[global_coefficient] = {"minimize": False}

Check warning on line 332 in src/ansys/simai/core/data/optimizations.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/simai/core/data/optimizations.py#L331-L332

Added lines #L331 - L332 were not covered by tests
return objective


# Undocumented for now, users don't really need to interact with it
class OptimizationTrialRunDirectory(Directory[OptimizationTrialRun]):
_data_model = OptimizationTrialRun
Expand All @@ -239,16 +341,10 @@
"""Get a specific trial run from the server."""
return self._model_from(self._client._api.get_optimization_trial_run(trial_run_id))

def _try_geometry(
self,
optimization: Identifiable[Optimization],
geometry: Identifiable[Geometry],
geometry_parameters: Dict,
def _run_iteration(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same method name for both optim and optimtrialrun objects, but with different args..

self, optimization: Identifiable[Optimization], parameters: Dict
) -> OptimizationTrialRun:
geometry_id = get_id_from_identifiable(geometry)
optimization_id = get_id_from_identifiable(optimization)
return self._model_from(
self._client._api.run_optimization_trial(
optimization_id, geometry_id, geometry_parameters
)
self._client._api.run_optimization_trial(optimization_id, parameters)
)
Loading
Loading