.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "_examples/01_pysimai_ex/04-non_parametric_optimization.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr__examples_01_pysimai_ex_04-non_parametric_optimization.py: .. _ref_non_parametric_optimization: Non-parametric optimization =========================== This example demonstrates how to perform non-parametric optimization in SimAI using the automorphing feature. Non-parametric optimization improves the performance of a baseline geometry by applying smooth, data-driven deformations based on predicted sensitivity maps. This approach is especially useful when there are no predefined design parameters, as it enables you to optimize the geometry directly by applying smooth and continuous deformations without generating new geometries or reparametrizing existing designs. Before you begin ---------------- Make sure you have: - A trained SimAI model with an associated workspace. - A baseline geometry uploaded to the workspace (supported formats: ``vtp``, ``stl``, ``zip``, ``cgns``). - Defined bounding boxes that specify regions where deformations can occur. - The ``ansys-simai-core`` library installed. - (Optional) The ``pyvista`` library (or your preferred CAD tool) installed for bounding box visualization. Key concepts ------------ **Baseline geometry**: The geometry to optimize. Choose a representative geometry from your dataset. **Bounding boxes**: Define the regions where deformations can occur. Bounding boxes are specified as ``[xmin, xmax, ymin, ymax, zmin, zmax]``. Start with boxes covering areas most likely to influence performance improvements. **Symmetries**: Optional constraints to reduce computational costs. Can be planar (e.g., ``["X"]`` for YZ plane symmetry) or axial. **Scalars**: Must correspond to the scalars defined in the SimAI workspace configuration. **Number of iterations**: Determines how many rounds of improvement are executed. Use 5 iterations for quick tests, or 100+ for production runs. **Objective**: The performance indicator to optimize (e.g., minimize drag, maximize lift). Only one objective can be defined for non-parametric optimization. **Offline token**: Required for server-side optimization. Allows the server to authenticate on your behalf while uploading geometries at each iteration. Generated via ``simai.me.generate_offline_token()`` and valid for 30 days. **Detail level**: Controls deformation refinement (integer from 1 to 10, default: 5). Low values produce coarse shape changes; high values allow fine local adjustments. **Part morphing**: Optional constraint that restricts deformation to specific geometry parts identified by a ``PartId`` cell field. .. GENERATED FROM PYTHON SOURCE LINES 82-86 Visualize bounding boxes (optional) ----------------------------------- Before running the optimization, you can visualize the bounding boxes on your geometry using PyVista to ensure they cover the correct regions. .. GENERATED FROM PYTHON SOURCE LINES 86-106 .. code-block:: Python import pyvista as pv # Path to the surface mesh file - replace with your geometry file path file_path = "" # Define bounding boxes as [xmin, xmax, ymin, ymax, zmin, zmax] BOUNDING_BOXES = [[-0.07, 0.15, -0.06, 0.12, -0.09, 0.15]] mesh = pv.read(file_path) plotter = pv.Plotter() plotter.add_mesh(mesh, color="white", show_edges=False) # Add bounding boxes to the scene for bounds in BOUNDING_BOXES: box = pv.Cube(bounds=bounds) plotter.add_mesh(box, color="lightblue", opacity=0.3) plotter.show() .. GENERATED FROM PYTHON SOURCE LINES 107-116 Run non-parametric optimization ---------------------------------- **This is the only section you need to edit.** Update the variables below to match your setup and the rest of the script will run automatically without any further changes. The one exception is if you want to **minimize** the objective instead of maximizing it: replace ``maximize=OBJECTIVE`` with ``minimize=OBJECTIVE`` in the ``run_non_parametric`` call further below. .. GENERATED FROM PYTHON SOURCE LINES 116-140 .. code-block:: Python import os import ansys.simai.core as asc from ansys.simai.core.data.predictions import Prediction ORGANIZATION_NAME = "" WORKSPACE_NAME = "" CHOSEN_GEOMETRY_NAME = "" # A bounding box must be defined as [xmin, xmax, ymin, ymax, zmin, zmax] BOUNDING_BOXES = [[-0.07, 0.15, -0.06, 0.12, -0.09, 0.15]] NUMBER_OF_ITERATIONS = 10 # Maximum displacement per bounding box (one value per box, same unit as geometry) MAX_DISPLACEMENT = [0.1] # Symmetry constraints (e.g., ["X"] for YZ plane symmetry) SYMMETRIES = [] # Objective to maximize (use minimize parameter for minimization) OBJECTIVE = [""] # Detail level controls deformation refinement (integer from 1 to 10, default: 5) DETAIL_LEVEL = 5 # Output folder for results OUTPUT_FOLDER = "simai_output" .. GENERATED FROM PYTHON SOURCE LINES 141-151 Part morphing (optional) ~~~~~~~~~~~~~~~~~~~~~~~~ Part morphing restricts deformation to specific parts of the geometry identified by a ``PartId`` cell field. The ``continuity_constraint`` parameter (0 to 1) controls how smoothly the deformed region blends with the rest. A value of 0 means no continuity enforcement; 1 gives the best continuity but reduces the overall deformation magnitude. Before adjusting ``detail_level`` to compensate, first experiment with different ``continuity_constraint`` values to find the right balance between interface smoothness and deformation magnitude. .. GENERATED FROM PYTHON SOURCE LINES 151-159 .. code-block:: Python from ansys.simai.core.data.optimizations import OptimizationPartMorphingSchema PART_MORPHING = OptimizationPartMorphingSchema( part_ids=[0], # IDs matching the ``PartId`` cell field continuity_constraint=0.5, # 0 = no constraint, 1 = maximum continuity ) .. GENERATED FROM PYTHON SOURCE LINES 160-163 Initialize SimAI client and workspace ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Connect to SimAI and retrieve the workspace and baseline geometry: .. GENERATED FROM PYTHON SOURCE LINES 163-172 .. code-block:: Python simai = asc.SimAIClient(organization=ORGANIZATION_NAME) workspace = simai.workspaces.get(name=WORKSPACE_NAME) print(f"Using workspace: {workspace.name}") geometry = simai.geometries.get(workspace=workspace, name=CHOSEN_GEOMETRY_NAME) print(f"Baseline geometry: {geometry.name}") .. GENERATED FROM PYTHON SOURCE LINES 173-183 Generate an offline token ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The optimization runs server-side. At each iteration the geometry corresponding to the current step is uploaded to your workspace. An ``offline_token`` is required so that the server can authenticate on your behalf during this process. Generating the token requires a manual action (browser login). Once generated, the token is valid for 30 days. You can generate as many tokens as needed. .. GENERATED FROM PYTHON SOURCE LINES 183-186 .. code-block:: Python offline_token = simai.me.generate_offline_token() .. GENERATED FROM PYTHON SOURCE LINES 187-192 Start the optimization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Launch the optimization with the configured parameters. The optimization will generate new geometries at each iteration by applying smooth deformations to the baseline geometry. .. GENERATED FROM PYTHON SOURCE LINES 192-208 .. code-block:: Python optimization = simai.optimizations.run_non_parametric( geometry=geometry, offline_token=offline_token, bounding_boxes=BOUNDING_BOXES, max_displacement=MAX_DISPLACEMENT, scalars={}, # Scalars must match your workspace configuration n_iters=NUMBER_OF_ITERATIONS, symmetries=SYMMETRIES, detail_level=DETAIL_LEVEL, maximize=OBJECTIVE, # Use 'minimize' parameter for minimization objectives show_progress=True, part_morphing=PART_MORPHING, # Comment to disable part morphing ) optimization.wait() .. GENERATED FROM PYTHON SOURCE LINES 209-212 Display optimization results ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Print the optimization ID, generated geometries, and objective values: .. GENERATED FROM PYTHON SOURCE LINES 212-217 .. code-block:: Python optimization_id = optimization.optimization.id print(f"Optimization ID: {optimization_id}") print(f"Generated geometries: {[geo.name for geo in optimization.list_geometries()]}") print(f"Objectives: {optimization.list_objectives()}") .. GENERATED FROM PYTHON SOURCE LINES 218-222 Run predictions on optimized geometries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can get back your optimization from its ID when done asynchronously. Run predictions on all generated geometries to evaluate their performance: .. GENERATED FROM PYTHON SOURCE LINES 222-230 .. code-block:: Python predictions: list[Prediction] = [] for geom in optimization.list_geometries(): print(f"Running prediction for: {geom.name} (ID: {geom.id})") prediction = geom.run_prediction(scalars={}) # Scalars must match your workspace configuration predictions.append(prediction) .. GENERATED FROM PYTHON SOURCE LINES 231-234 Analyze the results from an optimization run ---------------------------------------------- Download surface VTP files for each prediction: .. GENERATED FROM PYTHON SOURCE LINES 234-250 .. code-block:: Python VTPS_FOLDER = f"{OUTPUT_FOLDER}/optimization_{optimization_id}/VTPs" os.makedirs(VTPS_FOLDER, exist_ok=True) for prediction in predictions: # Wait for prediction to complete prediction.wait() geom = prediction.geometry geom_name = geom.name.split(".")[0] print(f"Downloading results for: {geom.name} (ID: {geom.id})") surface_vtp = prediction.post.surface_vtp() surface_vtp.data.download(f"{VTPS_FOLDER}/{geom_name}_surface.vtp") print("All results downloaded successfully!") .. GENERATED FROM PYTHON SOURCE LINES 251-255 Visualize optimization progression ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Plot the objective value at each iteration alongside the baseline and ±5 % reference bands, then plot mean and maximum displacement per iteration. .. GENERATED FROM PYTHON SOURCE LINES 255-259 .. code-block:: Python import matplotlib.pyplot as plt import numpy as np .. GENERATED FROM PYTHON SOURCE LINES 260-264 Get the baseline objective value ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Run a prediction on the original baseline geometry to obtain the reference objective value used for the ±5 % horizontal bands. .. GENERATED FROM PYTHON SOURCE LINES 264-276 .. code-block:: Python CHARTS_FOLDER = f"{OUTPUT_FOLDER}/optimization_{optimization_id}/charts" os.makedirs(CHARTS_FOLDER, exist_ok=True) print("Running baseline prediction for visualization reference...") baseline_pred = geometry.run_prediction() baseline_pred.wait() baseline_gc_data = baseline_pred.post.global_coefficients().data baseline_value = float(baseline_gc_data[OBJECTIVE[0]]["data"]) print(f"Baseline {OBJECTIVE[0]}: {baseline_value:.4f}") .. GENERATED FROM PYTHON SOURCE LINES 277-280 Collect per-iteration objective values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The optimization records the scalar objective at each iteration. .. GENERATED FROM PYTHON SOURCE LINES 280-286 .. code-block:: Python raw_objectives = optimization.list_objectives() obj_values = [float(obj[OBJECTIVE[0]]) for obj in raw_objectives] iterations = range(1, len(obj_values) + 1) .. GENERATED FROM PYTHON SOURCE LINES 287-290 Chart 1 – Objective progression over iterations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The dashed and dotted horizontal lines mark a ±5 % band around the baseline. .. GENERATED FROM PYTHON SOURCE LINES 290-319 .. code-block:: Python fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(iterations, obj_values, marker="o", linewidth=2, label=OBJECTIVE[0]) ax.axhline(baseline_value, color="black", linewidth=1.5, linestyle="-", label="Baseline") ax.axhline( baseline_value * 1.05, color="gray", linewidth=1, linestyle="--", label="Baseline +5 %", ) ax.axhline( baseline_value * 0.95, color="gray", linewidth=1, linestyle=":", label="Baseline \u22125 %", ) ax.set_xlabel("Iteration") ax.set_ylabel(OBJECTIVE[0]) ax.set_title("Objective progression over optimization iterations") ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(f"{CHARTS_FOLDER}/objective_progression.png", dpi=150) plt.show() .. GENERATED FROM PYTHON SOURCE LINES 320-327 Collect per-iteration displacement statistics ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Read the surface VTP file for each generated geometry and extract the ``Delta`` field. Nodes that did not move (value == 0) are filtered out because many surface nodes are intentionally frozen by the bounding-box constraints. Mean and maximum magnitude are then computed from the remaining active nodes. .. GENERATED FROM PYTHON SOURCE LINES 327-346 .. code-block:: Python geom_list = optimization.list_geometries() mean_displacements = [] max_displacements = [] for geom in geom_list: geom_name = geom.name.split(".")[0] vtp_path = f"{VTPS_FOLDER}/{geom_name}_surface.vtp" mesh = pv.read(vtp_path) delta = mesh.point_data["Delta"] # Compute per-node magnitude magnitude = np.linalg.norm(delta, axis=1) # Exclude nodes that did not move by design nonzero = magnitude[magnitude > 0] mean_displacements.append(float(nonzero.mean()) if nonzero.size > 0 else 0.0) max_displacements.append(float(nonzero.max()) if nonzero.size > 0 else 0.0) .. GENERATED FROM PYTHON SOURCE LINES 347-352 Chart 2 – Displacement statistics over iterations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shows how the surface deformation evolves iteration by iteration. A growing maximum displacement indicates the optimizer is making increasingly bold geometry changes; a plateau suggests convergence. .. GENERATED FROM PYTHON SOURCE LINES 352-382 .. code-block:: Python fig, ax = plt.subplots(figsize=(10, 5)) ax.plot( iterations, mean_displacements, marker="o", linewidth=2, label="Mean displacement (active nodes)", ) ax.plot( iterations, max_displacements, marker="s", linewidth=2, linestyle="--", label="Max displacement (active nodes)", ) ax.set_xlabel("Iteration") ax.set_ylabel("Delta magnitude") ax.set_title("Surface displacement statistics over optimization iterations") ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(f"{CHARTS_FOLDER}/displacement_statistics.png", dpi=150) plt.show() print(f"Charts saved to {CHARTS_FOLDER}.") .. GENERATED FROM PYTHON SOURCE LINES 383-401 Tips for effective optimization ------------------------------------------------------ **Data preparation**: Ensure the maximum magnitude within simulation data matches the minimal optimization gain you want. For example, for a 4% drag reduction, ensure there is around 4% drag difference between the best and worst training data. **Bounding boxes**: Start with boxes covering areas most likely to influence performance. If results do not improve, increase box size or add more boxes. Avoid excessively large boxes that may cause unrealistic deformations. **Baseline geometry**: If optimization does not improve after several iterations, try a different geometry from the training data. Ensure the geometry is clean and of high quality. **Iterative workflow**: For best results, run the optimization, verify results with your solver, add optimums as new training data, rebuild the model, and repeat at least 3 times. .. GENERATED FROM PYTHON SOURCE LINES 403-412 Next steps ------------------------------------------------------ - Visualize the surface VTP files. - Run solver verification on selected optimized geometries. - Add validated results as new training data for model improvement. For more details on configuration options, see the :ref:`automorphing configuration guide `. .. GENERATED FROM PYTHON SOURCE LINES 414-421 Other tools: Generate screenshots and GIFs ------------------------------------------------------ For every optimized geometry, render a screenshot from each camera angle. Then assemble one GIF per angle so the deformation is clearly visible. Requirements: ``pip install imageio`` .. GENERATED FROM PYTHON SOURCE LINES 421-510 .. code-block:: Python import imageio.v3 as iio SCREENSHOTS_FOLDER = f"{OUTPUT_FOLDER}/optimization_{optimization_id}/screenshots" GIFS_FOLDER = f"{OUTPUT_FOLDER}/optimization_{optimization_id}/GIFs" os.makedirs(SCREENSHOTS_FOLDER, exist_ok=True) os.makedirs(GIFS_FOLDER, exist_ok=True) # Output image resolution (width, height) in pixels IMG_SIZE = (1920, 1080) # Visual style BACKGROUND_COLOR = "white" MESH_COLOR = "lightgray" GIF_FRAME_DURATION_MS = 120 # Named camera angles CAMERA_ANGLES = { "front": ((0, -1, 0), (0, 0, 1)), "rear": ((0, 1, 0), (0, 0, 1)), "left": ((-1, 0, 0), (0, 0, 1)), "right": ((1, 0, 0), (0, 0, 1)), "top": ((0, 0, 1), (0, 1, 0)), "bottom": ((0, 0, -1), (0, 1, 0)), "isometric": ((1, 1, 1), (0, 0, 1)), } def _apply_camera(plotter: pv.Plotter, camera_angle: str) -> None: """Point the camera in the direction specified by ``camera_angle``.""" position_dir, view_up = CAMERA_ANGLES[camera_angle] plotter.reset_camera() focal = np.array(plotter.camera.focal_point) dist = plotter.camera.distance pos_dir = np.array(position_dir, dtype=float) pos_dir /= np.linalg.norm(pos_dir) plotter.camera.position = focal + pos_dir * dist plotter.camera.focal_point = focal plotter.camera.up = view_up def screenshot_vtp( vtp_path: str, png_path: str, camera_angle: str = "isometric", ) -> None: """Render a PNG screenshot with a camera framed on the global bounding box.""" mesh = pv.read(vtp_path) plotter = pv.Plotter(off_screen=True, window_size=IMG_SIZE) plotter.background_color = BACKGROUND_COLOR plotter.add_mesh(mesh, color=MESH_COLOR, show_edges=False) _apply_camera(plotter, camera_angle) plotter.camera.zoom(1.3) plotter.screenshot(png_path) plotter.close() # --------------------------------------------------------------------------- # Render screenshots and GIFs # --------------------------------------------------------------------------- all_vtp_paths = [ f"{VTPS_FOLDER}/{geom.name.split('.')[0]}_surface.vtp" for geom in optimization.list_geometries() ] png_index: dict[str, list[str]] = {angle: [] for angle in CAMERA_ANGLES} for vtp_path in all_vtp_paths: stem = os.path.splitext(os.path.basename(vtp_path))[0] for angle_name in CAMERA_ANGLES: png_path = f"{SCREENSHOTS_FOLDER}/{stem}_{angle_name}.png" screenshot_vtp(vtp_path, png_path, camera_angle=angle_name) png_index[angle_name].append(png_path) print(f" Screenshot → {png_path}") for angle_name, png_paths in png_index.items(): if not png_paths: continue gif_path = f"{GIFS_FOLDER}/{angle_name}.gif" frames = [iio.imread(p) for p in png_paths] iio.imwrite(gif_path, frames, duration=GIF_FRAME_DURATION_MS, loop=0) print(f" GIF ({len(frames)} frames) → {gif_path}") print("\nAll screenshots and GIFs saved.") .. _sphx_glr_download__examples_01_pysimai_ex_04-non_parametric_optimization.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: 04-non_parametric_optimization.ipynb <04-non_parametric_optimization.ipynb>` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: 04-non_parametric_optimization.py <04-non_parametric_optimization.py>` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: 04-non_parametric_optimization.zip <04-non_parametric_optimization.zip>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_