diff --git a/examples/mapdl_motorbike_frame/exec_mapdl.py b/examples/mapdl_motorbike_frame/exec_mapdl.py index d177c32ab..82512e323 100644 --- a/examples/mapdl_motorbike_frame/exec_mapdl.py +++ b/examples/mapdl_motorbike_frame/exec_mapdl.py @@ -51,7 +51,7 @@ def execute(self): # Add " around exe if needed for Windows exe = app["executable"] if " " in exe and not exe.startswith('"'): - exe = '"%s"' % exe # noqa + exe = f'"{exe}"' # noqa # Use properties from resource requirements num_cores = self.context.resource_requirements["num_cores"] diff --git a/examples/python_large_output/exec_python.py b/examples/python_large_output/exec_python.py index ca24ae468..882520038 100644 --- a/examples/python_large_output/exec_python.py +++ b/examples/python_large_output/exec_python.py @@ -49,7 +49,7 @@ def execute(self): # Add " around exe if needed for Windows exe = app["executable"] if " " in exe and not exe.startswith('"'): - exe = '"%s"' % exe # noqa + exe = f'"{exe}"' # noqa # Use properties from resource requirements # None currently diff --git a/examples/python_multi_process_step/eval.py b/examples/python_multi_process_step/eval.py index beab09a95..9c999c2d4 100644 --- a/examples/python_multi_process_step/eval.py +++ b/examples/python_multi_process_step/eval.py @@ -161,6 +161,8 @@ def text(i, txt): f"{task_definition}", "--in-subscript", ] + if "images": + cmd.append("--images") log.info("Run Subscript with: {cmd}") subprocess.run(cmd, check=False) diff --git a/examples/python_multi_process_step/project_setup.py b/examples/python_multi_process_step/project_setup.py index 8d7374524..a378be0f7 100644 --- a/examples/python_multi_process_step/project_setup.py +++ b/examples/python_multi_process_step/project_setup.py @@ -24,22 +24,20 @@ Author(s): R.Walker -Run *python eval.py --help* for command line arguments. +Run *python project_setup.py --help* for command line arguments. The project id is generated as -"py_{NUM_PROCESS_STEPS}_ps" and the `_img` is appended if result image is written. - -Per default the project is inactive. You can activate the project with the `-a` flag +"Python - {NUM_PROCESS_STEPS} Task Defs (- Img )- Sequential/Parallel" Example: ------- ``` -python project_setup.py -n 100 -c 10 --no-images +python project_setup.py -n 100 -c 10 ``` -Create 100 design points +Create 100 design points with the default 3 tasks each and change the first 10 design points - and do not write an result image. + and do not write a result image. """ @@ -235,7 +233,7 @@ def main( Software(name="Python", version=python_version), ], execution_command=cmd, - max_execution_time=duration * 1.5, + max_execution_time=duration * 1.5 + 12.0, resource_requirements=ResourceRequirements( num_cores=0.2, memory=100 * 1024 * 1024, # 100 MB @@ -280,7 +278,7 @@ def main( # change dp task files if change_job_tasks > 0: log.info(f"Change tasks for {change_job_tasks} jobs") - update_task_files(proj, change_job_tasks, images) + update_task_files(project_api, change_job_tasks, images) log.info(f"Created project '{proj.name}', ID='{proj.id}'") diff --git a/examples/python_multi_process_step/task_files.py b/examples/python_multi_process_step/task_files.py index 18aa13883..f1277265f 100644 --- a/examples/python_multi_process_step/task_files.py +++ b/examples/python_multi_process_step/task_files.py @@ -54,7 +54,8 @@ def update_input_file(task): def update_eval_script(task): - path = "eval.py" + cwd = os.path.dirname(__file__) + path = os.path.join(cwd, "eval.py") lines = open(path).readlines() log.info(f"Update input file {path} for task {task.task_definition_snapshot.name}") @@ -82,14 +83,21 @@ def update_eval_script(task): def update_task_files(project_api, num_jobs, write_images): log.debug("=== Update Task files ===") - config = project_api.get_job_definitions()[0] - jobs = config.get_jobs(limit=num_jobs) + jobs = project_api.get_jobs(limit=num_jobs) + + # Stop the jobs we're about to change + for job in jobs: + job["eval_status"] = "inactive" + project_api.update_jobs(jobs) + for job in jobs: + job["eval_status"] = "pending" for dp in jobs: log.debug(f" Update job {dp.name}") dp.name = dp.name + " Modified" - tasks = dp.get_tasks() - for i, task in enumerate(tasks, 1): + tasks = project_api.get_tasks() + tasks_sel = [t for t in tasks if t.job_id == dp.id] + for i, task in enumerate(tasks_sel): files = [] # input_file ##input_name = "ps_{}_input".format(i) @@ -99,18 +107,18 @@ def update_task_files(project_api, num_jobs, write_images): ## src=os.path.join(cwd, "input.json".format(i)) ) ) # new input_file will be used by subprocess new_input_file = update_input_file(task) - new_input_name = f"sub_td_{i}_input" + new_input_name = f"sub_td{i}_input" files.append( File( name=new_input_name, - evaluation_path=f"sub_td_{i}_input.json", + evaluation_path=f"sub_td{i}_input.json", type="application/json", src=new_input_file, ) ) # overwrite the eval script: same name --> will be overwritten new_eval_script = update_eval_script(task) - new_eval_name = f"td_{i}_pyscript" + new_eval_name = f"td{i}_pyscript" files.append( File( name=new_eval_name, @@ -126,11 +134,11 @@ def update_task_files(project_api, num_jobs, write_images): # collect=True, monitor=True, # type="text/plain" ) ) # new output text - new_out_name = f"sub_td_{i}_results" + new_out_name = f"sub_td{i}_results" files.append( File( name=new_out_name, - evaluation_path=f"sub_td_{i}_results.txt", + evaluation_path=f"sub_td{i}_results.txt", collect=True, monitor=True, type="text/plain", @@ -138,11 +146,11 @@ def update_task_files(project_api, num_jobs, write_images): ) # new image if write_images: - new_image_name = f"sub_td_{i}_results_jpg" + new_image_name = f"sub_td{i}_results_jpg" files.append( File( name=new_image_name, - evaluation_path=f"sub_td_{i}_results.jpg", + evaluation_path=f"sub_td{i}_results.jpg", type="image/jpeg", collect=True, ) @@ -158,7 +166,7 @@ def update_task_files(project_api, num_jobs, write_images): task.output_file_ids = output_file_ids task.input_file_ids = [file_ids[new_input_name], file_ids[new_eval_name]] - project_api.update_tasks(tasks) + project_api.update_tasks(tasks_sel) project_api.update_jobs(jobs) log.info(f"Updated {len(jobs)} design points") diff --git a/examples/python_two_bar_truss_params_in_exec_script/evaluate.py b/examples/python_two_bar_truss_params_in_exec_script/evaluate.py new file mode 100644 index 000000000..24ff8b244 --- /dev/null +++ b/examples/python_two_bar_truss_params_in_exec_script/evaluate.py @@ -0,0 +1,92 @@ +# Copyright (C) 2022 - 2025 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. + +# ruff: noqa + +import json +import math +import os +import sys + + +def weight(P, d, t, B, H, rho, E): + w = 2 * math.pi * rho * d * t * math.sqrt((0.5 * B) ** 2 + H**2) + return w + + +def stress(P, d, t, B, H, rho, E): + s = P * math.sqrt((0.5 * B) ** 2 + H**2) / (2 * t * math.pi * d * H) + return s / 1000 # psi to ksi + + +def buckling_stress(P, d, t, B, H, rho, E): + b = math.pi**2 * E * (d**2 + t**2) / (8 * ((0.5 * B) ** 2 + H**2)) + return b / 1000 # psi to ksi + + +def deflection(P, d, t, B, H, rho, E): + f = P * math.pow((0.5 * B) ** 2 + H**2, 3 / 2) / (2 * t * math.pi * d * H**2 * E) + return f + + +def filter_params(params): + for key in ["weight", "stress", "buckling_stress", "deflection"]: + params.pop(key) + + +def rename_params(params): + param_name_to_label = { + "height": "H", + "diameter": "d", + "thickness": "t", + "separation_distance": "B", + "young_modulus": "E", + "density": "rho", + "load": "P", + } + for key in param_name_to_label.keys(): + params[param_name_to_label[key]] = params.pop(key) + + +def main(): + # Load, filter and rename parameters + input_file_name = sys.argv[1] + input_file_path = os.path.abspath(input_file_name) + with open(input_file_path) as input_file: + params = json.load(input_file) + filter_params(params) + rename_params(params) + + # Calculate results + output_parameters = {} + output_parameters.update(weight=weight(**params)) + output_parameters.update(stress=stress(**params)) + output_parameters.update(buckling_stress=buckling_stress(**params)) + output_parameters.update(deflection=deflection(**params)) + + # Output results + with open("output_parameters.json", "w") as out_file: + json.dump(output_parameters, out_file, indent=4) + + +if __name__ == "__main__": + main() diff --git a/examples/python_two_bar_truss_params_in_exec_script/exec_python.py b/examples/python_two_bar_truss_params_in_exec_script/exec_python.py new file mode 100644 index 000000000..6911ef7c0 --- /dev/null +++ b/examples/python_two_bar_truss_params_in_exec_script/exec_python.py @@ -0,0 +1,82 @@ +# Copyright (C) 2022 - 2025 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. + +"""Simplistic execution script for Python. + +Command formed: python +""" + +import json +import os + +from ansys.rep.common.logging import log +from ansys.rep.evaluator.task_manager import ApplicationExecution + + +class PythonExecution(ApplicationExecution): + def execute(self): + log.info("Start Python execution script") + + # Identify files + script_file = next((f for f in self.context.input_files if f["name"] == "script"), None) + assert script_file, "Python script file script missing" + input_filename = "input_parameters.json" + output_filename = "output_parameters.json" + + with open(input_filename, "w") as in_file: + json.dump(self.context.parameter_values, in_file, indent=4) + + # Identify application + app_name = "Python" + app = next((a for a in self.context.software if a["name"] == app_name), None) + assert app, f"Cannot find app {app_name}" + + # Add " around exe if needed for Windows + exe = app["executable"] + if " " in exe and not exe.startswith('"'): + exe = f'"{exe}"' # noqa + + # Use properties from resource requirements + # None currently + + # Pass env vars correctly + env = dict(os.environ) + env.update(self.context.environment) + + # Form command + cmd = f"{exe} {script_file['path']} {input_filename}" + + log.debug(f"Executing command: {cmd}") + # Execute + self.run_and_capture_output(cmd, shell=True, env=env) + + # Extract parameters if needed + try: + log.debug(f"Loading output parameters from {output_filename}") + with open(output_filename) as out_file: + output_parameters = json.load(out_file) + self.context.parameter_values.update(output_parameters) + log.debug(f"Loaded output parameters: {output_parameters}") + except Exception as ex: + log.error(f"Failed to read output_parameters from file: {ex}") + + log.info("End Python execution script") diff --git a/examples/python_two_bar_truss_params_in_exec_script/project_setup.py b/examples/python_two_bar_truss_params_in_exec_script/project_setup.py new file mode 100644 index 000000000..11419af13 --- /dev/null +++ b/examples/python_two_bar_truss_params_in_exec_script/project_setup.py @@ -0,0 +1,229 @@ +# Copyright (C) 2022 - 2025 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. + +"""Python two-bar truss example with direct parameter exchange.""" + +import argparse +import logging +import os +import random + +from ansys.hps.client import Client, HPSError +from ansys.hps.client.jms import ( + File, + FitnessDefinition, + FloatParameterDefinition, + JmsApi, + Job, + JobDefinition, + Project, + ProjectApi, + ResourceRequirements, + Software, + SuccessCriteria, + TaskDefinition, +) + +log = logging.getLogger(__name__) + + +def main(client, num_jobs, python_version=None) -> Project: + """This example demonstrates how parameters can directly be exchanged with the application from within the execution script.""" # noqa + log.debug("=== Project") + proj = Project(name="Two-bar Truss Problem", priority=1, active=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) + + log.debug("=== Files") + cwd = os.path.dirname(__file__) + files = [ + File( + name="script", + evaluation_path="evaluate.py", + type="text/plain", + src=os.path.join(cwd, "evaluate.py"), + ), + ] + files.append( + File( + name="exec_python", + evaluation_path="exec_python.py", + type="application/x-python-code", + src=os.path.join(cwd, "exec_python.py"), + ) + ) + files = project_api.create_files(files) + file_ids = {f.name: f.id for f in files} + input_file_ids = [file_ids["script"]] + + log.debug("=== Job Definition with simulation workflow and parameters") + job_def = JobDefinition( + name="JobDefinition.1", + active=True, + ) + + # Input params + params = [ + FloatParameterDefinition( + name="height", lower_limit=10, upper_limit=100.0, default=30, units="in", mode="input" + ), + FloatParameterDefinition( + name="diameter", lower_limit=0.2, upper_limit=5, default=3, units="in", mode="input" + ), + FloatParameterDefinition( + name="thickness", + lower_limit=0.03, + upper_limit=0.6, + default=0.15, + units="in", + mode="input", + ), + FloatParameterDefinition( + name="separation_distance", + lower_limit=40, + upper_limit=150, + default=60, + units="in", + mode="input", + ), + FloatParameterDefinition( + name="young_modulus", + lower_limit=1e6, + upper_limit=1e8, + default=3e7, + units="lbs in^-2", + mode="input", + ), + FloatParameterDefinition( + name="density", + lower_limit=0.1, + upper_limit=0.6, + default=0.3, + units="lbs in^-2", + mode="input", + ), + FloatParameterDefinition( + name="load", lower_limit=1e1, upper_limit=1e5, default=66e3, units="lbs", mode="input" + ), + ] + # Output params + params.extend( + [ + FloatParameterDefinition(name="weight", units="lbs", mode="output"), + FloatParameterDefinition(name="stress", units="ksi", mode="output"), + FloatParameterDefinition(name="buckling_stress", units="ksi", mode="output"), + FloatParameterDefinition(name="deflection", units="in", mode="output"), + ] + ) + params = project_api.create_parameter_definitions(params) + job_def.parameter_definition_ids = [o.id for o in params] + + task_def = TaskDefinition( + name="python_evaluation", + software_requirements=[Software(name="Python", version=python_version)], + resource_requirements=ResourceRequirements( + num_cores=0.5, + ), + use_execution_script=True, + execution_script_id=file_ids["exec_python"], + execution_level=0, + max_execution_time=30.0, + input_file_ids=input_file_ids, + success_criteria=SuccessCriteria( + return_code=0, + require_all_output_parameters=True, + ), + ) + + task_def = project_api.create_task_definitions([task_def])[0] + job_def.task_definition_ids = [task_def.id] + + # Fitness definition + fd = FitnessDefinition(error_fitness=10.0) + fd.add_fitness_term( + name="weight", + type="design_objective", + weighting_factor=1.0, + expression="map_design_objective( values['weight'], 35, 20)", + ) + fd.add_fitness_term( + name="max_stress", + type="limit_constraint", + weighting_factor=1.0, + expression="map_limit_constraint( values['stress'], 85, 10 )", + ) + fd.add_fitness_term( + name="max_deflection", + type="limit_constraint", + weighting_factor=1.0, + expression="map_limit_constraint( values['deflection'], 0.25, 0.05 )", + ) + job_def.fitness_definition = fd + + # Create job_definition in project + job_def = project_api.create_job_definitions([job_def])[0] + + # Refresh parameters + params = project_api.get_parameter_definitions(job_def.parameter_definition_ids) + + log.debug("=== Jobs") + jobs = [] + for i in range(num_jobs): + values = { + p.name: p.lower_limit + random.random() * (p.upper_limit - p.lower_limit) + for p in params + if p.mode == "input" + } + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) + + log.info(f"Created project '{proj.name}', ID='{proj.id}'") + + return proj + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-U", "--url", default="https://127.0.0.1:8443/hps") + parser.add_argument("-u", "--username", default="repuser") + parser.add_argument("-p", "--password", default="repuser") + parser.add_argument("-n", "--num-jobs", type=int, default=50) + parser.add_argument("-v", "--python-version", default="3.10") + + args = parser.parse_args() + + logger = logging.getLogger() + logging.basicConfig(format="[%(asctime)s | %(levelname)s] %(message)s", level=logging.DEBUG) + + client = Client(url=args.url, username=args.username, password=args.password) + + try: + main( + client, + num_jobs=args.num_jobs, + python_version=args.python_version, + ) + except HPSError as e: + log.error(str(e)) diff --git a/tests/test_examples.py b/tests/test_examples.py index 7c508e435..1091863e2 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -141,6 +141,21 @@ def test_python_two_bar_truss_problem_with_exec_script(client): jms_api.delete_project(project) +def test_python_two_bar_truss_params_in_exec_script(client): + from examples.python_two_bar_truss_params_in_exec_script.project_setup import main + + num_jobs = 10 + project = main(client, num_jobs) + assert project is not None + + jms_api = JmsApi(client) + project_api = ProjectApi(client, project.id) + + assert len(project_api.get_jobs()) == num_jobs + + jms_api.delete_project(project) + + def test_mapdl_linked_analyses(client): from examples.mapdl_linked_analyses.project_setup import create_project