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

CHORE: Second pass of oss code review #37

Merged
merged 12 commits into from
May 28, 2024
Merged
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ repos:
rev: 6.3.0
hooks:
- id: pydocstyle
additional_dependencies: [toml]
additional_dependencies: [tomli]
exclude: "tests/"

- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/37.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CHORE: Second pass of oss code review
File renamed without changes.
204 changes: 204 additions & 0 deletions examples/simple_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Copyright (C) 2023 - 2024 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.

"""This example shows basic usage of PyConceptEV Core."""

import datetime
import os
from pathlib import Path

import plotly.graph_objects as go

from ansys.conceptev.core import app

# Setup required environment variables
os.environ["CONCEPTEV_URL"] = "https://conceptev.ansys.com/api/"
os.environ["OCM_URL"] = "https://prod.portal.onscale.com/api"
os.environ["CONCEPTEV_USERNAME"] = "joe.blogs@my_work.com"
os.environ["CONCEPTEV_PASSWORD"] = "sup3r_s3cr3t_passw0rd"

# Uncomment the following lines of code if you prefer to use local .env file
# import dotenv
# dotenv.load_dotenv()

# Various constant data
DATADIR = Path(__file__).parents[0] / "resources"
MOTOR_FILE_NAME = str(DATADIR.joinpath("e9.lab"))

# Example data can be obtained from the schema sections of the API documentation.
AERO_1 = {
"name": "New Aero Config",
"drag_coefficient": 0.3,
"cross_sectional_area": 2,
"config_type": "aero",
}
AERO_2 = {
"name": "Second Aero Configuration",
"drag_coefficient": 0.6,
"cross_sectional_area": 3,
"config_type": "aero",
}
MASS = {
"name": "New Mass Config",
"mass": 3000,
"config_type": "mass",
}
WHEEL = {
"name": "New Wheel Config",
"rolling_radius": 0.3,
"config_type": "wheel",
}
TRANSMISSION = {
"gear_ratios": [5],
"headline_efficiencies": [0.95],
"max_torque": 500,
"max_speed": 2000,
"static_drags": [0.5],
"friction_ratios": [60],
"windage_ratios": [40],
"component_type": "TransmissionLossCoefficients",
}
BATTERY = {
"capacity": 86400000,
"charge_acceptance_limit": 0,
"component_type": "BatteryFixedVoltages",
"internal_resistance": 0.1,
"name": "New Battery",
"voltage_max": 400,
"voltage_mid": 350,
"voltage_min": 300,
}

# Motor data
motor_data = {"name": "e9", "component_type": "MotorLabID", "inverter_losses_included": False}

# Get a token from OCM
token = app.get_token()
print(token)

# Create a new project

with app.get_http_client(token) as client:
health = app.get(client, "/health")
print(f"API is healthy: {health}")

concepts = app.get(client, "/concepts")
print(f"List of concepts: {concepts}")

accounts = app.get_account_ids(token)
print(f"Account IDs: {accounts}")

account_id = accounts[os.environ["CONCEPTEV_USERNAME"]]
hpc_id = app.get_default_hpc(token, account_id)
print(f"HPC ID: {hpc_id}")

project = app.create_new_project(
client, account_id, hpc_id, f"New Project +{datetime.datetime.now()}"
)
print(f"ID of the created project: {project['id']}")

# Perform basic operations on the design instance associated to the new project

design_instance_id = project["design_instance_id"]
with app.get_http_client(token, design_instance_id) as client:

# Create configurations
created_aero = app.post(client, "/configurations", data=AERO_1)
created_aero2 = app.post(client, "/configurations", data=AERO_2)
created_mass = app.post(client, "/configurations", data=MASS)
created_wheel = app.post(client, "/configurations", data=WHEEL)

# Read all aero configurations
configurations = app.get(client, "/configurations", params={"config_type": "aero"})
print(f"List of configurations: {configurations}")

# Get a specific aero configuration
aero = app.get(client, "/configurations", id=created_aero["id"])
print(f"First created areo configuration: {aero}")

# Create component
created_transmission = app.post(client, "/components", data=TRANSMISSION)

# Create component from file
motor_loss_map = app.post_component_file(client, MOTOR_FILE_NAME, "motor_lab_file")
motor_data["data_id"] = motor_loss_map[0]
motor_data["max_speed"] = motor_loss_map[1]

created_motor = app.post(client, "/components", data=motor_data)
print(f"Created motor: {created_motor}")

# Extend client timeout to get loss map from the motor
client.timeout = 2000
motor_loss_map = app.post(
client,
"/components:get_display_data",
data={},
params={"component_id": created_motor["id"]},
)

# Show a figure of the loss map from the motor in you browser
x = motor_loss_map["currents"]
y = motor_loss_map["phase_advances"]
z = motor_loss_map["losses_total"]
fig = go.Figure(data=go.Contour(x=x, y=y, z=z))
fig.show()

created_battery = app.post(client, "/components", data=BATTERY)

# Create an architecture
architecture = {
"number_of_front_wheels": 1,
"number_of_front_motors": 1,
"front_transmission_id": created_transmission["id"],
"front_motor_id": created_motor["id"],
"number_of_rear_wheels": 0,
"number_of_rear_motors": 0,
"battery_id": created_battery["id"],
}
created_arch = app.post(client, "/architectures", data=architecture)
print(f"Created architecture: {created_arch}")

# Create a requirement
requirement = {
"speed": 10,
"acceleration": 1,
"aero_id": created_aero["id"],
"mass_id": created_mass["id"],
"wheel_id": created_wheel["id"],
"state_of_charge": 0.9,
"requirement_type": "static_acceleration",
"name": "Static Requirement 1",
}
created_requirement = app.post(client, "requirements", data=requirement)
print(f"Created requirement: {created_requirement}")

# Create and submit a job
concept = app.get(client, "/concepts", id=design_instance_id, params={"populated": True})
job_info = app.create_submit_job(client, concept, account_id, hpc_id)

# Read the results and show the result in your browser
results = app.read_results(client, job_info, calculate_units=False)
x = results[0]["capability_curve"]["speeds"]
y = results[0]["capability_curve"]["torques"]

fig = go.Figure(data=go.Scatter(x=x, y=y))
fig.show()
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,14 @@ def get_http_client(token: str, design_instance_id: str | None = None) -> httpx.
re-creating this connection for each call.
"""
base_url = os.environ["CONCEPTEV_URL"]
params = None
if design_instance_id:
params = {"design_instance_id": design_instance_id}
else:
params = None
return httpx.Client(headers={"Authorization": token}, params=params, base_url=base_url)


def processed_response(response) -> dict:
"""Process response.
def process_response(response) -> dict:
"""Process a response.

Check the value returned from the API and raise an error if the process is not successful.
"""
Expand All @@ -97,7 +96,7 @@ def processed_response(response) -> dict:
def get(
client: httpx.Client, router: Router, id: str | None = None, params: dict | None = None
) -> dict:
"""Get/read from the client at the specific route.
"""Send a GET request to the base client.

This HTTP verb performs the ``GET`` request and adds the route to the base client.
"""
Expand All @@ -106,20 +105,20 @@ def get(
else:
path = router
response = client.get(url=path, params=params)
return processed_response(response)
return process_response(response)


def post(client: httpx.Client, router: Router, data: dict, params: dict = {}) -> dict:
"""Post/create from the client at the specific route.
"""Send a POST request to the base client.

This HTTP verb performs the ``POST`` request and adds the route to the base client.
"""
response = client.post(url=router, json=data, params=params)
return processed_response(response)
return process_response(response)


def delete(client: httpx.Client, router: Router, id: str) -> dict:
"""Delete from the client at the specific route.
"""Send a DELETE request to the base client.

This HTTP verb performs the ``DELETE`` request and adds the route to the base client.
"""
Expand All @@ -129,6 +128,16 @@ def delete(client: httpx.Client, router: Router, id: str) -> dict:
raise Exception(f"Failed to delete from {router} with ID:{id}.")


def put(client: httpx.Client, router: Router, id: str, data: dict) -> dict:
"""Put/update from the client at the specific route.

An HTTP verb that performs the ``PUT`` request and adds the route to the base client.
"""
path = "/".join([router, id])
response = client.put(url=path, json=data)
return process_response(response)


def create_new_project(
client: httpx.Client,
account_id: str,
Expand Down Expand Up @@ -167,12 +176,12 @@ def create_new_project(
osm_url + "/design/create", headers={"Authorization": token}, json=design_data
)

if created_design.status_code != 200 and created_design.status_code != 204:
if created_design.status_code not in (200, 204):
raise Exception(f"Failed to create a design on OCM {created_design.content}.")

user_details = httpx.post(osm_url + "/user/details", headers={"Authorization": token})
if user_details.status_code != 200 and user_details.status_code != 204:
raise Exception(f"Failed to get user details {user_details}.")
if user_details.status_code not in (200, 204):
raise Exception(f"Failed to get a user details on OCM {user_details}.")

concept_data = {
"capabilities_ids": [],
Expand Down Expand Up @@ -250,16 +259,6 @@ def create_submit_job(
return job_info


def put(client: httpx.Client, router: Router, id: str, data: dict) -> dict:
"""Put/update from the client at the specific route.

An HTTP verb that performs the ``PUT`` request and adds the route to the base client.
"""
path = "/".join([router, id])
response = client.put(url=path, json=data)
return processed_response(response)


def read_file(filename: str) -> str:
"""Read a given file."""
with open(filename) as f:
Expand All @@ -274,9 +273,13 @@ def read_results(
no_of_tries: int = 200,
rate_limit: float = 0.3,
) -> dict:
"""Keep trying for results. If results aren't completed, try again."""
"""Read job results.

Continuously request job results until a valid response is received or a limit of tries is
reached.
"""
version_number = get(client, "/utilities:data_format_version")
for i in range(0, no_of_tries):
for _ in range(0, no_of_tries):
response = client.post(
url="/jobs:result",
json=job_info,
Expand All @@ -293,7 +296,7 @@ def read_results(


def post_component_file(client: httpx.Client, filename: str, component_file_type: str) -> dict:
"""Post/create from the client at the specific route with a file.
"""Send a POST request to the base client with a file.

An HTTP verb that performs the ``POST`` request, adds the route to the base client,
and then adds the file as a multipart form request.
Expand All @@ -303,7 +306,7 @@ def post_component_file(client: httpx.Client, filename: str, component_file_type
response = client.post(
url=path, files={"file": file_contents}, params={"component_file_type": component_file_type}
)
return processed_response(response)
return process_response(response)


if __name__ == "__main__":
Expand Down
Loading