Note
Go to the bottom of this page to download the ZIP file for the uv example.
Run arbitrary Python scripts on HPS#
This example shows how to run arbitrary Python scripts. It uses the uv package to generate the required environments on the fly.
The example sets up a project that plots sin(x) using NumPy and
Matplotlib and then saves the figure to a file.
The metadata header present in the eval.py script, which defines the
dependencies, enables uv to take care of the environment setup:
# /// script
# requires-python = "==3.12"
# dependencies = [
# "numpy",
# "matplotlib"
# ]
# ///
For more information, see Inline script metadata in the Python Packaging User Guide and Running a script with dependencies in the uv documentation.
Prerequisites#
For the example to run, uv must be installed and registered on the scaler/evaluator. For installation instructions, see Installing uv in the uv documentation.
Once uv is installed, the package must be registered in the scaler/evaluator with the following properties:
Property |
Value |
|---|---|
Name |
uv |
Version |
0.7.19 |
Installation Path |
/path/to/uv |
Executable |
/path/to/uv/bin/uv |
Be sure to adjust the version to the one you have installed.
Define a custom cache directory#
The preceding steps set up uv with the cache located in its default location in the user home directory (~/.cache/uv). Depending on your situation, you might prefer a different cache location, such as a shared directory accessible to all evaluators. To define a custom uv cache directory, add the following environment variable to the uv package registration in the scaler/evaluator:
Environment Variable |
Value |
|---|---|
UV_CACHE_DIR |
/path/to/custom/uv/cache/dir |
Create offline air-gapped setups#
If internet is not available, you can create offline air-gapped setups for uv using one of these options:
Pre-populate the uv cache with all desired dependencies.
Provide a local Python package index and set uv to use it. For more information, see Package indexes in the uv documentation. This index can then sit in a shared location, with node-local caching applied.
Use pre-generated virtual environments. For more information, see uv venv in the uv documentation.
To turn off network access, you can either set the
UV_OFFLINE environment variable or use the --offline flag with
many uv commands.
Run the example#
To run the example, execute the project_setup.py script:
uv run project_setup.py
This command sets up a project with a number
of jobs. Each job generates a plot.png file.
Options#
The example supports the following command line arguments:
Flag |
Example |
Description |
|---|---|---|
|
|
URL of the target HPS instance |
|
|
Username to log into HPS |
|
|
Password to log into HPS |
|
|
Number of jobs to generate |
Files#
Descriptions follow of the relevant example files.
The project creation script, project_setup.py, handles all communication with the
HPS instance, defines the project, and generates the jobs.
"""Python with uv example."""
import argparse
import logging
import os
from ansys.hps.client import Client, HPSError
from ansys.hps.client.jms import (
File,
JmsApi,
Job,
JobDefinition,
Project,
ProjectApi,
ResourceRequirements,
Software,
SuccessCriteria,
TaskDefinition,
)
log = logging.getLogger(__name__)
def create_project(client, num_jobs):
log.debug("=== Create Project")
jms_api = JmsApi(client)
proj = jms_api.create_project(
Project(
name=f"Python UV example - {num_jobs} jobs",
priority=1,
active=True,
),
replace=True,
)
project_api = ProjectApi(client, proj.id)
log.debug("=== Define Files")
cwd = os.path.dirname(__file__)
# Input Files
files = [
File(
name="eval",
evaluation_path="eval.py",
type="text/plain",
src=os.path.join(cwd, "eval.py"),
),
File(
name="exec_script",
evaluation_path="exec_script.py",
type="text/plain",
src=os.path.join(cwd, "exec_script.py"),
),
File(
name="plot",
evaluation_path="plot.png",
type="image/png",
collect=True,
),
]
files = project_api.create_files(files)
file_ids = {f.name: f.id for f in files}
log.debug("=== Define Task")
task_def = TaskDefinition(
name="plotting",
software_requirements=[Software(name="uv")],
resource_requirements=ResourceRequirements(
num_cores=0.5,
memory=100 * 1024 * 1024, # 100 MB
disk_space=10 * 1024 * 1024, # 10 MB
),
execution_level=0,
max_execution_time=500.0,
use_execution_script=True,
execution_script_id=file_ids["exec_script"],
execution_command="%executable% run %file:eval%",
input_file_ids=[file_ids["eval"]],
output_file_ids=[file_ids["plot"]],
success_criteria=SuccessCriteria(
return_code=0,
require_all_output_files=True,
),
)
task_defs = project_api.create_task_definitions([task_def])
print("== Define Job")
job_def = JobDefinition(
name="JobDefinition.1", active=True, task_definition_ids=[task_defs[0].id]
)
job_def = project_api.create_job_definitions([job_def])[0]
log.debug(f"== Create {num_jobs} Jobs")
jobs = []
for i in range(num_jobs):
jobs.append(Job(name=f"Job.{i}", eval_status="pending", job_definition_id=job_def.id))
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("-j", "--num-jobs", type=int, default=10)
parser.add_argument("-U", "--url", default="https://localhost:8443/hps")
parser.add_argument("-u", "--username", default="repuser")
parser.add_argument("-p", "--password", default="repuser")
args = parser.parse_args()
logger = logging.getLogger()
logging.basicConfig(format="%(message)s", level=logging.DEBUG)
try:
log.info("Connect to HPC Platform Services")
client = Client(url=args.url, username=args.username, password=args.password)
log.info(f"HPS URL: {client.url}")
proj = create_project(
client=client,
num_jobs=args.num_jobs,
)
except HPSError as e:
log.error(str(e))
The script eval.py, which is evaluated on HPS, contains the code to plot a sine and then save
the figure.
import matplotlib.pyplot as plt
import numpy as np
if __name__ == "__main__":
# Generate plot
ts = np.linspace(0.0, 10.0, 100)
ys = np.sin(ts)
fig, ax = plt.subplots()
ax.plot(ts, ys)
ax.set_xlabel("Time [s]")
ax.set_ylabel("Displacement [cm]")
plt.savefig("plot.png", dpi=200)
# Uncomment to enable venv cleanup in exec script, see execution script for details
# import json
# import sys
# with open("output_parameters.json", "w") as out_file:
# json.dump({"exe": sys.executable}, out_file, indent=4)
The execution script, exec_script.py, uses uv to run the evaluation script.
"""Simple execution script for Python with uv.
Command formed: uv run <script_file>
"""
import json
import os
import shutil
from ansys.rep.common.logging import log
from ansys.rep.evaluator.task_manager import ApplicationExecution
class PythonExecution(ApplicationExecution):
def execute(self):
log.info("Start uv execution script")
# Identify files
script_file = next((f for f in self.context.input_files if f["name"] == "eval"), None)
assert script_file, "Python script file missing"
output_filename = "output_parameters.json"
# Identify applications
app_uv = next((a for a in self.context.software if a["name"] == "uv"), None)
assert app_uv, "Cannot find app uv"
# Add " around exe if needed for Windows
exes = {"uv": app_uv["executable"]}
for k, v in exes.items():
if " " in v and not v.startswith('"'):
exes[k] = f'"{v}"' # noqa
# Pass env vars correctly
env = dict(os.environ)
env.update(self.context.environment)
## Run evaluation script
cmd = f"{exes['uv']} run {script_file['path']}"
self.run_and_capture_output(cmd, shell=True, env=env)
# Read eval.py output parameters
output_parameters = {}
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.info("No output parameters found.")
log.debug(f"Failed to read output_parameters from file: {ex}")
# If exe path is in out params, delete the venv folder to avoid runaway uv venv cache
# See https://github.com/astral-sh/uv/issues/13431
if "exe" in output_parameters.keys():
try:
venv_cache = os.path.abspath(os.path.join(output_parameters["exe"], "..", ".."))
if os.path.exists(venv_cache):
log.debug(f"Cleaning venv cache at {venv_cache}...")
shutil.rmtree(venv_cache)
else:
log.debug(f"Venv cache path {venv_cache} does not exist.")
except Exception as ex:
log.debug(f"Couldn't clean venv cache at {venv_cache}: {ex}")
log.info("End Python execution script")
Download the ZIP file for the uv example and use a tool such as 7-Zip to extract the files.