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

Update parametric study data as simulations are running. #486

Merged
merged 5 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
* Default version of Additive Server is 25.1 when creating a client connection.
* Single bead simulations with thermal history are now supported.
* Added description field to AdditiveMaterial.
* Updated the parametric runner to accept a list of simulation ids to be run.
* Updated the parametric runner to run a list of simulation ids.
* Asynchronous simulate functionality added.
* Added apply and list server settings functionality.
* Parametric study data is updated as simulations are running.
* Use LOG for logging in Additive object.

### Bug Fixes

Expand Down
73 changes: 31 additions & 42 deletions src/ansys/additive/core/additive.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
# SOFTWARE.
"""Provides a client for interacting with the Additive service."""

from datetime import datetime
import logging
import os
import time

from ansys.api.additive import __version__ as api_version
from ansys.api.additive.v0.additive_materials_pb2 import (
Expand Down Expand Up @@ -99,8 +99,9 @@ class Additive:
2024 R1 would be specified as ``241``. This parameter is only applicable in
`PyPIM`_-enabled cloud environments and on localhost. Using an empty string
or ``None`` uses the default product version.
log_level: str, default: "INFO"
Minimum severity level of messages to log.
log_level: str, default: ""
Minimum severity level of messages to log. Valid values are "DEBUG", "INFO",
"WARNING", "ERROR", and "CRITICAL". The default value equates to "WARNING".
log_file: str, default: ""
File name to write log messages to.
enable_beta_features: bool, default: False
Expand Down Expand Up @@ -145,7 +146,7 @@ def __init__(
nsims_per_server: int = 1,
nservers: int = 1,
product_version: str = DEFAULT_PRODUCT_VERSION,
log_level: str = "INFO",
log_level: str = "",
log_file: str = "",
enable_beta_features: bool = False,
linux_install_path: os.PathLike | None = None,
Expand All @@ -154,11 +155,19 @@ def __init__(
if not product_version:
product_version = DEFAULT_PRODUCT_VERSION

self._log = Additive._create_logger(log_file, log_level)
self._log.debug("Logging set to %s", log_level)
if log_level:
LOG.setLevel(log_level)
if log_file:
LOG.log_to_file(filename=log_file, level=log_level)

self._servers = Additive._connect_to_servers(
server_connections, host, port, nservers, product_version, self._log, linux_install_path
server_connections,
host,
port,
nservers,
product_version,
LOG,
linux_install_path,
)

initial_settings = {"NumConcurrentSims": str(nsims_per_server)}
Expand All @@ -172,28 +181,6 @@ def __init__(
os.makedirs(self._user_data_path)
LOG.info("user data path: " + self._user_data_path)

@staticmethod
def _create_logger(log_file, log_level) -> logging.Logger:
"""Instantiate the logger."""
format = "%(asctime)s %(message)s"
datefmt = "%Y-%m-%d %H:%M:%S"
numeric_level = getattr(logging, log_level.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % log_level)
logging.basicConfig(
level=numeric_level,
format=format,
datefmt=datefmt,
)
log = logging.getLogger(__name__)
if log_file:
file_handler = logging.FileHandler(str(log_file))
file_handler.setLevel(numeric_level)
file_handler.setFormatter(logging.Formatter(format))
log.file_handler = file_handler
log.addHandler(file_handler)
return log

@staticmethod
def _connect_to_servers(
server_connections: list[str | grpc.Channel] = None,
Expand Down Expand Up @@ -327,13 +314,9 @@ def simulate(
list is returned.
"""
summaries = []
task = self.simulate_async(inputs, progress_handler)
if isinstance(task, SimulationTaskManager):
task.wait_all()
summaries = task.summaries()
else:
task.wait()
summaries.append(task.summary)
task_mgr = self.simulate_async(inputs, progress_handler)
task_mgr.wait_all(progress_handler=progress_handler)
summaries = task_mgr.summaries()

for summ in summaries:
if isinstance(summ, SimulationError):
Expand Down Expand Up @@ -387,10 +370,7 @@ def simulate_async(
if len(inputs) == 0:
raise ValueError("No simulation inputs provided")

LOG.info(
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Starting {len(inputs)} simulations",
end="",
)
LOG.info(f"Starting {len(inputs)} simulations")
for i, sim_input in enumerate(inputs):
server_id = i % len(self._servers)
server = self._servers[server_id]
Expand Down Expand Up @@ -447,6 +427,7 @@ def _simulate(
simulation_task = SimulationTask(
server, long_running_op, simulation_input, self._user_data_path
)
LOG.debug(f"Simulation task created for {simulation_input.id}")

except Exception as e:
metadata = OperationMetadata(simulation_id=simulation_input.id, message=str(e))
Expand All @@ -455,6 +436,9 @@ def _simulate(
simulation_task = SimulationTask(
server, errored_op, simulation_input, self._user_data_path
)
if progress_handler:
time.sleep(0.1) # Allow time for the server to start the simulation
progress_handler.update(simulation_task.status())

return simulation_task

Expand Down Expand Up @@ -489,7 +473,9 @@ def material(self, name: str) -> AdditiveMaterial:

@staticmethod
def load_material(
parameters_file: str, thermal_lookup_file: str, characteristic_width_lookup_file: str
parameters_file: str,
thermal_lookup_file: str,
characteristic_width_lookup_file: str,
) -> AdditiveMaterial:
"""Load a custom material definition for the current session. The resulting
``AdditiveMaterial`` object will not be saved to the library.
Expand Down Expand Up @@ -525,7 +511,10 @@ def load_material(
return material

def add_material(
self, parameters_file: str, thermal_lookup_file: str, characteristic_width_lookup_file: str
self,
parameters_file: str,
thermal_lookup_file: str,
characteristic_width_lookup_file: str,
) -> AdditiveMaterial | None:
"""Add a custom material to the library for use in additive simulations.

Expand Down
31 changes: 24 additions & 7 deletions src/ansys/additive/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
file_path = os.path.join(os.getcwd(), "pyadditive.log")
LOG.log_to_file(file_path)
"""

import logging
import sys

Expand All @@ -79,6 +80,7 @@
# Formatting
STDOUT_MSG_FORMAT = "%(asctime)s - %(levelname)s - %(module)s - %(funcName)s - %(message)s"
FILE_MSG_FORMAT = STDOUT_MSG_FORMAT
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


def is_notebook() -> bool:
Expand Down Expand Up @@ -164,7 +166,7 @@ class Logger:
"""

file_handler = None
std_out_handler = None
stdout_handler = None
_level = logging.DEBUG
_instances = {}

Expand Down Expand Up @@ -242,6 +244,21 @@ def handle_exception(exc_type, exc_value, exc_traceback):

sys.excepthook = handle_exception

def setLevel(self, level: str | int) -> None:
"""Set the logging level for the logger.

Parameters
----------
level : str or int
Logging level to filter the message severity allowed in the logger.
If int, it must be one of the levels defined in the :obj:`~logging` module.
Valid string values are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``,
and ``"CRITICAL"``.
"""
self.logger.setLevel(level)
for handler in self.logger.handlers:
handler.setLevel(level)


def addfile_handler(logger, filename=FILE_NAME, level=LOG_LEVEL):
"""Add a file handler to the input.
Expand Down Expand Up @@ -290,16 +307,16 @@ def add_stdout_handler(logger, level=LOG_LEVEL):
Logger
:class:`Logger` or :class:`logging.Logger` object.
"""
std_out_handler = logging.StreamHandler()
std_out_handler.setLevel(level)
std_out_handler.setFormatter(PyAdditiveFormatter(STDOUT_MSG_FORMAT))
stdout_handler = logging.StreamHandler()
stdout_handler.setLevel(level)
stdout_handler.setFormatter(PyAdditiveFormatter(STDOUT_MSG_FORMAT, DATE_FORMAT))

if isinstance(logger, Logger):
logger.std_out_handler = std_out_handler
logger.logger.addHandler(std_out_handler)
logger.stdout_handler = stdout_handler
logger.logger.addHandler(stdout_handler)

elif isinstance(logger, logging.Logger):
logger.addHandler(std_out_handler)
logger.addHandler(stdout_handler)

return logger

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
class ParametricRunner:
"""Provides methods to run parametric study simulations."""

@staticmethod
def simulate(
self,
df: pd.DataFrame,
additive: Additive,
progress_handler: IProgressHandler = None,
Expand Down Expand Up @@ -98,7 +98,6 @@ def simulate(
continue

summaries = additive.simulate(inputs, progress_handler)

return summaries

@staticmethod
Expand Down
Loading
Loading