From 5c90520f4633164e7e33439615c8e7964427ba7a Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Thu, 29 Feb 2024 14:57:10 -0700 Subject: [PATCH 1/7] Add 3d microstructure simulation API --- .pre-commit-config.yaml | 12 +- pyproject.toml | 2 +- src/ansys/additive/core/__init__.py | 1 + src/ansys/additive/core/additive.py | 36 +- src/ansys/additive/core/microstructure.py | 132 +++-- src/ansys/additive/core/microstructure_3d.py | 447 +++++++++++++++++ src/ansys/additive/core/progress_logger.py | 8 +- tests/test_additive.py | 25 +- tests/test_microstructure.py | 7 +- tests/test_microstructure_3d.py | 501 +++++++++++++++++++ 10 files changed, 1095 insertions(+), 76 deletions(-) create mode 100644 src/ansys/additive/core/microstructure_3d.py create mode 100644 tests/test_microstructure_3d.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce7c5d28a..ea55e4bf6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,9 +64,9 @@ repos: args: [-vv, -m, -p, -S, -s, -n, --fail-under=100] files: src - - repo: https://github.com/ansys/pre-commit-hooks - rev: v0.2.9 - hooks: - - id: add-license-headers - args: - - --start_year=2023 + # - repo: https://github.com/ansys/pre-commit-hooks + # rev: v0.2.9 + # hooks: + # - id: add-license-headers + # args: + # - --start_year=2023 diff --git a/pyproject.toml b/pyproject.toml index a90365085..230ff9557 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-additive==1.6.5", + "ansys-api-additive==1.6.7", "ansys-platform-instancemanagement>=1.1.1", "dill>=0.3.7", "google-api-python-client>=1.7.11", diff --git a/src/ansys/additive/core/__init__.py b/src/ansys/additive/core/__init__.py index 1ffc3c6aa..f74650bc9 100644 --- a/src/ansys/additive/core/__init__.py +++ b/src/ansys/additive/core/__init__.py @@ -60,6 +60,7 @@ MicrostructureInput, MicrostructureSummary, ) +from ansys.additive.core.microstructure_3d import Microstructure3DInput, Microstructure3DSummary from ansys.additive.core.porosity import PorosityInput, PorositySummary from ansys.additive.core.simulation import SimulationError, SimulationStatus, SimulationType from ansys.additive.core.single_bead import ( diff --git a/src/ansys/additive/core/additive.py b/src/ansys/additive/core/additive.py index 4e5250a98..9a2ce652c 100644 --- a/src/ansys/additive/core/additive.py +++ b/src/ansys/additive/core/additive.py @@ -43,6 +43,7 @@ from ansys.additive.core.material import AdditiveMaterial from ansys.additive.core.material_tuning import MaterialTuningInput, MaterialTuningSummary from ansys.additive.core.microstructure import MicrostructureInput, MicrostructureSummary +from ansys.additive.core.microstructure_3d import Microstructure3DInput, Microstructure3DSummary import ansys.additive.core.misc as misc from ansys.additive.core.porosity import PorosityInput, PorositySummary from ansys.additive.core.progress_logger import ProgressLogger @@ -224,12 +225,20 @@ def about(self) -> None: def simulate( self, - inputs: SingleBeadInput | PorosityInput | MicrostructureInput | ThermalHistoryInput | list, + inputs: ( + SingleBeadInput + | PorosityInput + | MicrostructureInput + | ThermalHistoryInput + | Microstructure3DInput + | list + ), ) -> ( SingleBeadSummary | PorositySummary | MicrostructureSummary | ThermalHistorySummary + | Microstructure3DSummary | SimulationError | list ): @@ -237,19 +246,19 @@ def simulate( Parameters ---------- - inputs: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, list + inputs: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, + Microstructure3DInput, list Parameters to use for simulations. A list of inputs may be provided to execute multiple simulations. Returns ------- - SingleBeadSummary, PorositySummary, MicrostructureSummary, ThermalHistorySummary, SimulationError, - list + SingleBeadSummary, PorositySummary, MicrostructureSummary, ThermalHistorySummary, + Microstructure3DSummary, SimulationError, list One or more summaries of simulation results. If a list of inputs is provided, a list is returned. """ if type(inputs) is not list: - print("Single input") return self._simulate(inputs, self._servers[0], show_progress=True) else: self._validate_inputs(inputs) @@ -287,7 +296,13 @@ def simulate( def _simulate( self, - input: SingleBeadInput | PorosityInput | MicrostructureInput | ThermalHistoryInput, + input: ( + SingleBeadInput + | PorosityInput + | MicrostructureInput + | ThermalHistoryInput + | Microstructure3DInput + ), server: ServerConnection, show_progress: bool = False, ): @@ -295,7 +310,8 @@ def _simulate( Parameters ---------- - input: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput + input: SingleBeadInput, PorosityInput, MicrostructureInput, ThermalHistoryInput, + Microstructure3DInput Parameters to use for simulation. server: ServerConnection @@ -307,7 +323,7 @@ def _simulate( Returns ------- SingleBeadSummary, PorositySummary, MicrostructureSummary, ThermalHistorySummary, - SimulationError + Microstructure3DSummary, SimulationError """ logger = None if show_progress: @@ -337,6 +353,10 @@ def _simulate( return MicrostructureSummary( input, response.microstructure_result, self._user_data_path ) + if response.HasField("microstructure_3d_result"): + return Microstructure3DSummary( + input, response.microstructure_3d_result, self._user_data_path + ) except Exception as e: return SimulationError(input, str(e)) diff --git a/src/ansys/additive/core/microstructure.py b/src/ansys/additive/core/microstructure.py index 6087edfed..f8929a592 100644 --- a/src/ansys/additive/core/microstructure.py +++ b/src/ansys/additive/core/microstructure.py @@ -26,12 +26,15 @@ from ansys.api.additive.v0.additive_domain_pb2 import ( MicrostructureInput as MicrostructureInputMessage, ) -from ansys.api.additive.v0.additive_domain_pb2 import MicrostructureResult +from ansys.api.additive.v0.additive_domain_pb2 import ( + MicrostructureResult as MicrostructureResultMessage, +) from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest from google.protobuf.internal.containers import RepeatedCompositeFieldContainer import numpy as np import pandas as pd +from ansys.additive.core import misc from ansys.additive.core.machine import AdditiveMachine from ansys.additive.core.material import AdditiveMaterial @@ -506,47 +509,26 @@ class MicrostructureSummary: """Provides the summary of a microstructure simulation.""" def __init__( - self, input: MicrostructureInput, result: MicrostructureResult, user_data_path: str + self, input: MicrostructureInput, result: MicrostructureResultMessage, user_data_path: str ) -> None: """Initialize a ``MicrostructureSummary`` object.""" if not isinstance(input, MicrostructureInput): raise ValueError("Invalid input type passed to init, " + self.__class__.__name__) - if not isinstance(result, MicrostructureResult): + if not isinstance(result, MicrostructureResultMessage): raise ValueError("Invalid result type passed to init, " + self.__class__.__name__) - if not user_data_path or (user_data_path == ""): - raise ValueError("Invalid user data path passed to init, " + self.__class__.__name__) + if not user_data_path: + raise ValueError("Invalid user data path, " + self.__class__.__name__) self._input = input - self._output_path = os.path.join(user_data_path, input.id) - if not os.path.exists(self._output_path): - os.makedirs(self._output_path) - self._xy_vtk = os.path.join(self._output_path, "xy.vtk") - with open(self._xy_vtk, "wb") as xy_vtk: - xy_vtk.write(result.xy_vtk) - self._xz_vtk = os.path.join(self._output_path, "xz.vtk") - with open(self._xz_vtk, "wb") as xz_vtk: - xz_vtk.write(result.xz_vtk) - self._yz_vtk = os.path.join(self._output_path, "yz.vtk") - with open(self._yz_vtk, "wb") as yz_vtk: - yz_vtk.write(result.yz_vtk) + id = input.id if input.id else misc.short_uuid() + outpath = os.path.join(user_data_path, id) + self._result = _Microstructure2DResult(result, outpath) - self._xy_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( - result.xy_circle_equivalence - ) - self._xz_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( - result.xz_circle_equivalence - ) - self._yz_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( - result.yz_circle_equivalence - ) - self._xy_average_grain_size = MicrostructureSummary._average_grain_size( - self._xy_circle_equivalence - ) - self._xz_average_grain_size = MicrostructureSummary._average_grain_size( - self._xz_circle_equivalence - ) - self._yz_average_grain_size = MicrostructureSummary._average_grain_size( - self._yz_circle_equivalence - ) + def __repr__(self): + repr = type(self).__name__ + "\n" + for k in [x for x in self.__dict__ if x != "_result"]: + repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" + repr += self._result.__repr__() + return repr @property def input(self): @@ -558,18 +540,30 @@ def input(self): @property def xy_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XY plane.""" - return self._xy_vtk + """Path to the VTK file containing the 2-D grain structure data in the XY plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._result._xy_vtk @property def xz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XZ plane.""" - return self._xz_vtk + """Path to the VTK file containing the 2-D grain structure data in the XZ plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._result._xz_vtk @property def yz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the YZ plane.""" - return self._yz_vtk + """Path to the VTK file containing the 2-D grain structure data in the YZ plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._result._yz_vtk @property def xy_circle_equivalence(self) -> pd.DataFrame: @@ -577,7 +571,7 @@ def xy_circle_equivalence(self) -> pd.DataFrame: For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. """ - return self._xy_circle_equivalence + return self._result._xy_circle_equivalence @property def xz_circle_equivalence(self) -> pd.DataFrame: @@ -585,7 +579,7 @@ def xz_circle_equivalence(self) -> pd.DataFrame: For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. """ - return self._xz_circle_equivalence + return self._result._xz_circle_equivalence @property def yz_circle_equivalence(self) -> pd.DataFrame: @@ -593,22 +587,22 @@ def yz_circle_equivalence(self) -> pd.DataFrame: For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. """ - return self._yz_circle_equivalence + return self._result._yz_circle_equivalence @property def xy_average_grain_size(self) -> float: """Average grain size (µm) for the XY plane.""" - return self._xy_average_grain_size + return self._result._xy_average_grain_size @property def xz_average_grain_size(self) -> float: """Average grain size (µm) for the XZ plane.""" - return self._xz_average_grain_size + return self._result._xz_average_grain_size @property def yz_average_grain_size(self) -> float: """Average grain size (µm) for the YZ plane.""" - return self._yz_average_grain_size + return self._result._yz_average_grain_size @staticmethod def _circle_equivalence_frame(src: RepeatedCompositeFieldContainer) -> pd.DataFrame: @@ -641,8 +635,50 @@ def _average_grain_size(df: pd.DataFrame) -> float: * df[CircleEquivalenceColumnNames.AREA_FRACTION] ).sum() + +class _Microstructure2DResult: + """Provides the results of a 2D microstructure simulation.""" + + def __init__(self, msg: MicrostructureResultMessage, output_data_path: str) -> None: + """Initialize a ``Microstructure2DResult`` object.""" + if not isinstance(msg, MicrostructureResultMessage): + raise ValueError("Invalid msg parameter, " + self.__class__.__name__) + if not output_data_path: + raise ValueError("Invalid output data path, " + self.__class__.__name__) + + if not os.path.exists(output_data_path): + os.makedirs(output_data_path) + self._xy_vtk = os.path.join(output_data_path, "xy.vtk") + with open(self._xy_vtk, "wb") as xy_vtk: + xy_vtk.write(msg.xy_vtk) + self._xz_vtk = os.path.join(output_data_path, "xz.vtk") + with open(self._xz_vtk, "wb") as xz_vtk: + xz_vtk.write(msg.xz_vtk) + self._yz_vtk = os.path.join(output_data_path, "yz.vtk") + with open(self._yz_vtk, "wb") as yz_vtk: + yz_vtk.write(msg.yz_vtk) + + self._xy_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( + msg.xy_circle_equivalence + ) + self._xz_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( + msg.xz_circle_equivalence + ) + self._yz_circle_equivalence = MicrostructureSummary._circle_equivalence_frame( + msg.yz_circle_equivalence + ) + self._xy_average_grain_size = MicrostructureSummary._average_grain_size( + self._xy_circle_equivalence + ) + self._xz_average_grain_size = MicrostructureSummary._average_grain_size( + self._xz_circle_equivalence + ) + self._yz_average_grain_size = MicrostructureSummary._average_grain_size( + self._yz_circle_equivalence + ) + def __repr__(self): - repr = type(self).__name__ + "\n" + repr = "" for k in self.__dict__: repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" return repr diff --git a/src/ansys/additive/core/microstructure_3d.py b/src/ansys/additive/core/microstructure_3d.py new file mode 100644 index 000000000..94eb9db01 --- /dev/null +++ b/src/ansys/additive/core/microstructure_3d.py @@ -0,0 +1,447 @@ +# 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. +"""Provides input and result summary containers for microstructure 3D simulations.""" +import math +import os + +from ansys.api.additive.v0.additive_domain_pb2 import ( + Microstructure3DInput as Microstructure3DInputMessage, +) +from ansys.api.additive.v0.additive_domain_pb2 import Microstructure3DResult +from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest +import pandas as pd + +from ansys.additive.core import misc +from ansys.additive.core.machine import AdditiveMachine +from ansys.additive.core.material import AdditiveMaterial +from ansys.additive.core.microstructure import _Microstructure2DResult + + +class Microstructure3DInput: + """Provides input parameters for 3D microstructure simulation. + + Units are SI (m, kg, s, K) unless otherwise noted. + """ + + DEFAULT_POSITION_COORDINATE = 0 + """Default X, Y, Z, position coordinate (m).""" + MIN_POSITION_COORDINATE = 0 + """Minimum X, Y, Z, position coordinate (m).""" + MAX_POSITION_COORDINATE = 10 + """Maximum X, Y, Z, position coordinate (m).""" + DEFAULT_SAMPLE_SIZE = 0.1e-3 + """Default sample size in each dimension (m).""" + MIN_SAMPLE_SIZE = 15e-6 + """Minimum sample size in each dimension (m).""" + MAX_SAMPLE_SIZE = 0.5e-3 + """Maximum sample size in each dimension (m).""" + DEFAULT_RUN_INITIAL_MICROSTRUCTURE = True + """Default flag value indicating whether to run the initial microstructure conditions solver.""" + DEFAULT_USE_TRANSIENT_BULK_NUCLEATION = True + """Default flag value indicating whether to use transient bulk nucleation rather than initial microstructure conditions.""" # noqa: E501 + DEFAULT_NUMBER_OF_RANDOM_NUCLEI = 8000 + """Default number of random nuclei to use in the initial microstructure conditions solver.""" + DEFAULT_MAX_NUCLEATION_DENSITY_BULK = int(20e12) + """Default maximum nucleation density in the bulk (grains/m^3).""" + + def __init__( + self, + id: str = "", + *, + sample_min_x: float = DEFAULT_POSITION_COORDINATE, + sample_min_y: float = DEFAULT_POSITION_COORDINATE, + sample_min_z: float = DEFAULT_POSITION_COORDINATE, + sample_size_x: float = DEFAULT_SAMPLE_SIZE, + sample_size_y: float = DEFAULT_SAMPLE_SIZE, + sample_size_z: float = DEFAULT_SAMPLE_SIZE, + calculate_initial_microstructure: bool = DEFAULT_RUN_INITIAL_MICROSTRUCTURE, + use_transient_bulk_nucleation: bool = DEFAULT_USE_TRANSIENT_BULK_NUCLEATION, + max_bulk_nucleation_density: int = DEFAULT_MAX_NUCLEATION_DENSITY_BULK, + num_initial_random_nuclei: int = DEFAULT_NUMBER_OF_RANDOM_NUCLEI, + machine: AdditiveMachine = AdditiveMachine(), + material: AdditiveMaterial = AdditiveMaterial(), + ): + """Initialize a ``Microstructure3DInput`` object.""" + + self.id = id + self.sample_min_x = sample_min_x + self.sample_min_y = sample_min_y + self.sample_min_z = sample_min_z + self.sample_size_x = sample_size_x + self.sample_size_y = sample_size_y + self.sample_size_z = sample_size_z + self.calculate_initial_microstructure = calculate_initial_microstructure + self.use_transient_bulk_nucleation = use_transient_bulk_nucleation + self.max_bulk_nucleation_density = max_bulk_nucleation_density + self.num_initial_random_nuclei = num_initial_random_nuclei + self.machine = machine + self.material = material + + def __repr__(self): + repr = type(self).__name__ + "\n" + for k in self.__dict__: + if k == "_machine" or k == "_material": + repr += "\n" + k.replace("_", "", 1) + ": " + str(getattr(self, k)) + else: + repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" + return repr + + def __eq__(self, __o: object) -> bool: + if not isinstance(__o, Microstructure3DInput): + return False + return ( + self.id == __o.id + and self.sample_min_x == __o.sample_min_x + and self.sample_min_y == __o.sample_min_y + and self.sample_min_z == __o.sample_min_z + and self.sample_size_x == __o.sample_size_x + and self.sample_size_y == __o.sample_size_y + and self.sample_size_z == __o.sample_size_z + and self.calculate_initial_microstructure == __o.calculate_initial_microstructure + and self.use_transient_bulk_nucleation == __o.use_transient_bulk_nucleation + and self.max_bulk_nucleation_density == __o.max_bulk_nucleation_density + and self.num_initial_random_nuclei == __o.num_initial_random_nuclei + and self.machine == __o.machine + and self.material == __o.material + ) + + @staticmethod + def __validate_range(value, min, max, name): + if math.isnan(value): + raise ValueError("{} must be a number.".format(name)) + if value < min or value > max: + raise ValueError("{} must be between {} and {}.".format(name, min, max)) + + @staticmethod + def __validate_greater_than_zero(value, name): + if math.isnan(value): + raise ValueError("{} must be a number.".format(name)) + if value <= 0: + raise ValueError("{} must be greater than zero.".format(name)) + + @property + def id(self) -> str: + """User-provided ID for this simulation.""" + return self._id + + @id.setter + def id(self, value: str): + self._id = value + + @property + def machine(self): + """Machine-related parameters.""" + return self._machine + + @machine.setter + def machine(self, value): + self._machine = value + + @property + def material(self): + """Material used during simulation.""" + return self._material + + @material.setter + def material(self, value): + self._material = value + + @property + def sample_min_x(self) -> float: + """Minimum x coordinate of the geometry sample (m). + + Valid values are from the :obj:`MIN_POSITION_COORDINATE` value to + the :obj:`MAX_POSITION_COORDINATE` value. + """ + return self._sample_min_x + + @sample_min_x.setter + def sample_min_x(self, value: float): + self.__validate_range( + value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_x" + ) + self._sample_min_x = value + + @property + def sample_min_y(self) -> float: + """Minimum y coordinate of the geometry sample (m). + + Valid values are from the :obj:`MIN_POSITION_COORDINATE` value to the + :obj:`MAX_POSITION_COORDINATE` value. + """ + return self._sample_min_y + + @sample_min_y.setter + def sample_min_y(self, value: float): + self.__validate_range( + value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_y" + ) + self._sample_min_y = value + + @property + def sample_min_z(self) -> float: + """Minimum z coordinate of the geometry sample (m). + + Valid values are from the :obj:`MIN_POSITION_COORDINATE` value to the + :obj:`MAX_POSITION_COORDINATE` value. + """ + return self._sample_min_z + + @sample_min_z.setter + def sample_min_z(self, value: float): + self.__validate_range( + value, self.MIN_POSITION_COORDINATE, self.MAX_POSITION_COORDINATE, "sample_min_z" + ) + self._sample_min_z = value + + @property + def sample_size_x(self) -> float: + """Size of the geometry sample in the x direction (m). + + Valid values are from the :obj:`MIN_SAMPLE_SIZE` value to the + :obj:`MAX_SAMPLE_SIZE` value. + """ + return self._sample_size_x + + @sample_size_x.setter + def sample_size_x(self, value: float): + self.__validate_range(value, self.MIN_SAMPLE_SIZE, self.MAX_SAMPLE_SIZE, "sample_size_x") + self._sample_size_x = value + + @property + def sample_size_y(self) -> float: + """Size of the geometry sample in the y direction (m). + + Valid values are from the :obj:`MIN_SAMPLE_SIZE` value to the + :obj:`MAX_SAMPLE_SIZE` value. + """ + return self._sample_size_y + + @sample_size_y.setter + def sample_size_y(self, value: float): + self.__validate_range(value, self.MIN_SAMPLE_SIZE, self.MAX_SAMPLE_SIZE, "sample_size_y") + self._sample_size_y = value + + @property + def sample_size_z(self) -> float: + """Size of the geometry sample in the z direction (m). + + Valid values are from the :obj:`MIN_SAMPLE_SIZE` value to the + :obj:`MAX_SAMPLE_SIZE` value. + """ + return self._sample_size_z + + @sample_size_z.setter + def sample_size_z(self, value: float): + self.__validate_range(value, self.MIN_SAMPLE_SIZE, self.MAX_SAMPLE_SIZE, "sample_size_z") + self._sample_size_z = value + + @property + def calculate_initial_microstructure(self) -> bool: + """Whether to run the initial microstructure solver. + + If ``True``, the initial microstructure solver will be used to create initial + condition grain identifiers and euler angles. + If ``False``, the initial microstructure solver will not be run. + """ + return self._calculate_initial_microstructure + + @calculate_initial_microstructure.setter + def calculate_initial_microstructure(self, value: bool): + self._calculate_initial_microstructure = value + + @property + def use_transient_bulk_nucleation(self) -> bool: + """Allow nucleation in the bulk region of the meltpool. + + Nucleation rate is controlled by bulk nucleation density. + If ``True``, bulk nucleation is enabled. if ``False``, bulk + nucleation is disabled. + """ + return self._use_transient_bulk_nucleation + + @use_transient_bulk_nucleation.setter + def use_transient_bulk_nucleation(self, value: bool): + self._use_transient_bulk_nucleation = value + + @property + def max_bulk_nucleation_density(self) -> int: + """Maximum nucleation density in the bulk (grains/m^3). + + If ``use_transient_bulk_nucleation`` is ``False``, this value is ignored. + """ + return self._max_bulk_nucleation_density + + @max_bulk_nucleation_density.setter + def max_bulk_nucleation_density(self, value: int): + self.__validate_greater_than_zero(value, "max_bulk_nucleation_density") + self._max_bulk_nucleation_density = value + + @property + def num_initial_random_nuclei(self) -> int: + """Number of random nuclei to use for the microstructure initial conditions. + + This value will be used by the initial microstructure conditions solver. + If ``use_transient_bulk_nucleation`` is ``True``, this value is ignored. + """ + return self._num_initial_random_nuclei + + @num_initial_random_nuclei.setter + def num_initial_random_nuclei(self, value: int): + self.__validate_greater_than_zero(value, "num_initial_random_nuclei") + self._num_initial_random_nuclei = value + + def _to_simulation_request(self) -> SimulationRequest: + """Convert this object into a simulation request message.""" + input = Microstructure3DInputMessage( + machine=self.machine._to_machine_message(), + material=self.material._to_material_message(), + x_origin=self.sample_min_x, + y_origin=self.sample_min_y, + z_origin=self.sample_min_z, + x_length=self.sample_size_x, + y_length=self.sample_size_y, + z_length=self.sample_size_z, + use_transient_bulk_nucleation=self.use_transient_bulk_nucleation, + max_bulk_nucleation_density=float(self.max_bulk_nucleation_density), + num_random_nuclei=self.num_initial_random_nuclei, + run_initial_microstructure=self.calculate_initial_microstructure, + # TODO: Add support for user provided initial microstructure data + use_provided_initial_microstructure_data=False, + # TODO: Add support for user provided thermal data + ) + return SimulationRequest(id=self.id, microstructure_3d_input=input) + + +class Microstructure3DSummary: + """Provides the summary of a 3D microstructure simulation.""" + + _3D_GRAIN_VTK_NAME = "3d_grain_structure.vtk" + + def __init__( + self, input: Microstructure3DInput, result: Microstructure3DResult, user_data_path: str + ) -> None: + """Initialize a ``Microstructure3DSummary`` object.""" + if not isinstance(input, Microstructure3DInput): + raise ValueError("Invalid input type, " + self.__class__.__name__) + if not isinstance(result, Microstructure3DResult): + raise ValueError("Invalid result type, " + self.__class__.__name__) + if not user_data_path or (user_data_path == ""): + raise ValueError("Invalid user data path, " + self.__class__.__name__) + self._input = input + id = input.id if input.id else misc.short_uuid() + outpath = os.path.join(user_data_path, id) + os.makedirs(outpath, exist_ok=True) + self._2d_result = _Microstructure2DResult(result.two_d_result, outpath) + self._grain_3d_vtk = os.path.join(outpath, self._3D_GRAIN_VTK_NAME) + with open(self._grain_3d_vtk, "wb") as f: + f.write(result.three_d_vtk) + + def __repr__(self): + repr = type(self).__name__ + "\n" + for k in [x for x in self.__dict__ if x != "_2d_result"]: + repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" + repr += self._2d_result.__repr__() + return repr + + @property + def input(self): + """Simulation input. + + For more information, see the :class:`Microstructure3DInput` class. + """ + return self._input + + @property + def grain_3d_vtk(self) -> str: + """Path to the VTK file containing the 3-D grain structure data. + + The VTK file contains scalar data sets `GrainNumber`, `Phi0`, + `Phi1`, `Phi2` and `Temperatures`. + """ + return self._grain_3d_vtk + + @property + def xy_vtk(self) -> str: + """Path to the VTK file containing the 2-D grain structure data in the XY plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._2d_result._xy_vtk + + @property + def xz_vtk(self) -> str: + """Path to the VTK file containing the 2-D grain structure data in the XZ plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._2d_result._xz_vtk + + @property + def yz_vtk(self) -> str: + """Path to the VTK file containing the 2-D grain structure data in the YZ plane. + + The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and + `GrainNumber`. + """ + return self._2d_result._yz_vtk + + @property + def xy_circle_equivalence(self) -> pd.DataFrame: + """Circle equivalence data for the XY plane. + + For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. + """ + return self._2d_result._xy_circle_equivalence + + @property + def xz_circle_equivalence(self) -> pd.DataFrame: + """Circle equivalence data for the XZ plane. + + For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. + """ + return self._2d_result._xz_circle_equivalence + + @property + def yz_circle_equivalence(self) -> pd.DataFrame: + """Circle equivalence data for the YZ plane. + + For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. + """ + return self._2d_result._yz_circle_equivalence + + @property + def xy_average_grain_size(self) -> float: + """Average grain size (µm) for the XY plane.""" + return self._2d_result._xy_average_grain_size + + @property + def xz_average_grain_size(self) -> float: + """Average grain size (µm) for the XZ plane.""" + return self._2d_result._xz_average_grain_size + + @property + def yz_average_grain_size(self) -> float: + """Average grain size (µm) for the YZ plane.""" + return self._2d_result._yz_average_grain_size diff --git a/src/ansys/additive/core/progress_logger.py b/src/ansys/additive/core/progress_logger.py index 3313ddedf..6fdb27728 100644 --- a/src/ansys/additive/core/progress_logger.py +++ b/src/ansys/additive/core/progress_logger.py @@ -55,7 +55,13 @@ def log_progress(self, progress: Progress): # Don't send progress when generating docs return if not hasattr(self, "_pbar"): - self._pbar = tqdm(total=100, colour="green", desc=self._last_context, mininterval=0.001) + self._pbar = tqdm( + total=100, + colour="green", + desc=self._last_context, + # mininterval=0.001, + dynamic_ncols=True, + ) if progress.message and "SOLVERINFO" in progress.message: self._log.debug(progress.message) diff --git a/tests/test_additive.py b/tests/test_additive.py index 29a0c1ba9..fcef4516b 100644 --- a/tests/test_additive.py +++ b/tests/test_additive.py @@ -30,6 +30,7 @@ from ansys.api.additive import __version__ as api_version import ansys.api.additive.v0.about_pb2_grpc from ansys.api.additive.v0.additive_domain_pb2 import ( + Microstructure3DResult, MicrostructureResult, PorosityResult, Progress, @@ -54,6 +55,8 @@ from ansys.additive.core import ( USER_DATA_PATH, Additive, + Microstructure3DInput, + Microstructure3DSummary, MicrostructureInput, MicrostructureSummary, PorosityInput, @@ -304,6 +307,7 @@ def test_about_prints_server_status_messages(capsys: pytest.CaptureFixture[str]) PorosityInput(), MicrostructureInput(), ThermalHistoryInput(), + Microstructure3DInput(), ], ) # patch needed for Additive() call @@ -359,13 +363,11 @@ def test_simulate_with_input_list_calls_internal_simulate_n_times(_): additive = Additive() additive._simulate = _simulate_patch inputs = [ - x - for x in [ - SingleBeadInput(id="id1"), - PorosityInput(id="id2"), - MicrostructureInput(id="id3"), - ThermalHistoryInput(id="id4"), - ] + SingleBeadInput(id="id1"), + PorosityInput(id="id2"), + MicrostructureInput(id="id3"), + ThermalHistoryInput(id="id4"), + Microstructure3DInput(id="id5"), ] # act additive.simulate(inputs) @@ -475,6 +477,7 @@ def test_internal_simulate_with_thermal_history_without_geometry_returns_Simulat (SingleBeadInput(), MeltPoolMessage(), SingleBeadSummary), (PorosityInput(), PorosityResult(), PorositySummary), (MicrostructureInput(), MicrostructureResult(), MicrostructureSummary), + (Microstructure3DInput(), Microstructure3DResult(), Microstructure3DSummary), ], ) # patch needed for Additive() call @patch("ansys.additive.core.additive.ServerConnection") @@ -500,6 +503,10 @@ def test_internal_simulate_returns_correct_summary( sim_response = SimulationResponse( id="id", progress=progress_msg, microstructure_result=result ) + elif isinstance(result, Microstructure3DResult): + sim_response = SimulationResponse( + id="id", progress=progress_msg, microstructure_3d_result=result + ) else: assert False, "Invalid result type" @@ -521,6 +528,7 @@ def test_internal_simulate_returns_correct_summary( PorosityInput(), MicrostructureInput(), ThermalHistoryInput(), + Microstructure3DInput(), ], ) # patch needed for Additive() call @@ -540,6 +548,7 @@ def test_internal_simulate_without_material_raises_exception(_, input): SingleBeadInput(), PorosityInput(), MicrostructureInput(), + Microstructure3DInput(), ], ) # patch needed for Additive() call @@ -682,7 +691,7 @@ def test_tune_material_raises_exception_if_output_path_exists(_, tmp_path: pathl # act, assert with pytest.raises(ValueError, match="already exists"): - result = additive.tune_material(input, out_dir=tmp_path) + additive.tune_material(input, out_dir=tmp_path) @patch("ansys.additive.core.additive.ServerConnection") diff --git a/tests/test_microstructure.py b/tests/test_microstructure.py index 570f21195..0b997d061 100644 --- a/tests/test_microstructure.py +++ b/tests/test_microstructure.py @@ -97,7 +97,7 @@ def test_MicrostructureSummary_init_raises_exception_for_invalid_input_type( invalid_obj, ): # arrange, act, assert - with pytest.raises(ValueError, match="Invalid input type") as exc_info: + with pytest.raises(ValueError, match="Invalid input type"): MicrostructureSummary(invalid_obj, MicrostructureResult(), ".") @@ -113,7 +113,7 @@ def test_MicrostructureSummary_init_raises_exception_for_invalid_result_type( invalid_obj, ): # arrange, act, assert - with pytest.raises(ValueError, match="Invalid result type") as exc_info: + with pytest.raises(ValueError, match="Invalid result type"): MicrostructureSummary(MicrostructureInput(), invalid_obj, ".") @@ -128,7 +128,7 @@ def test_MicrostructureSummary_init_raises_exception_for_invalid_path( invalid_path, ): # arrange, act, assert - with pytest.raises(ValueError, match="Invalid user data path") as exc_info: + with pytest.raises(ValueError, match="Invalid user data path"): MicrostructureSummary(MicrostructureInput(), MicrostructureResult(), invalid_path) @@ -545,7 +545,6 @@ def test_MicrostructureSummary_repr_returns_expected_string(): + "thermal_properties_data: ThermalPropertiesDataPoint[]\n" + "random_seed: 0\n" + "\n" - + f"output_path: {expected_output_dir}\n" + "xy_vtk: " + os.path.join(expected_output_dir, "xy.vtk") + "\n" diff --git a/tests/test_microstructure_3d.py b/tests/test_microstructure_3d.py new file mode 100644 index 000000000..3ee242e20 --- /dev/null +++ b/tests/test_microstructure_3d.py @@ -0,0 +1,501 @@ +# 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. + +import math +import os +import shutil +import tempfile + +from ansys.api.additive.v0.additive_domain_pb2 import ( + GrainStatistics, + Microstructure3DResult, + MicrostructureResult, +) +from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest +import pytest + +from ansys.additive.core.machine import AdditiveMachine +from ansys.additive.core.material import AdditiveMaterial +from ansys.additive.core.microstructure_3d import Microstructure3DInput, Microstructure3DSummary + + +def test_Microstructure3DSummary_init_returns_expected_value(): + # arrange + user_data_path = os.path.join(tempfile.gettempdir(), "microstructure_3d_summary_init") + os.makedirs(user_data_path, exist_ok=True) + input = Microstructure3DInput(id="id") + xy_vtk_bytes = bytes(range(3)) + xz_vtk_bytes = bytes(range(4, 6)) + yz_vtk_bytes = bytes(range(7, 9)) + grain_3d_bytes = bytes(range(10, 12)) + xy_stats = GrainStatistics(grain_number=1, area_fraction=2, diameter_um=3, orientation_angle=4) + xz_stats = GrainStatistics(grain_number=5, area_fraction=6, diameter_um=7, orientation_angle=8) + yz_stats = GrainStatistics( + grain_number=9, area_fraction=10, diameter_um=11, orientation_angle=12 + ) + result = MicrostructureResult(xy_vtk=xy_vtk_bytes, xz_vtk=xz_vtk_bytes, yz_vtk=yz_vtk_bytes) + result.xy_circle_equivalence.append(xy_stats) + result.xz_circle_equivalence.append(xz_stats) + result.yz_circle_equivalence.append(yz_stats) + result_3d = Microstructure3DResult(three_d_vtk=grain_3d_bytes, two_d_result=result) + + # act + summary = Microstructure3DSummary(input, result_3d, user_data_path) + + # assert + assert isinstance(summary, Microstructure3DSummary) + assert input == summary.input + assert summary.grain_3d_vtk == os.path.join(user_data_path, "id", summary._3D_GRAIN_VTK_NAME) + assert summary.xy_vtk == os.path.join(user_data_path, "id", "xy.vtk") + assert os.path.exists(summary.xy_vtk) + assert summary.xz_vtk == os.path.join(user_data_path, "id", "xz.vtk") + assert os.path.exists(summary.xz_vtk) + assert summary.yz_vtk == os.path.join(user_data_path, "id", "yz.vtk") + assert os.path.exists(summary.yz_vtk) + assert summary.xy_circle_equivalence["grain_number"][0] == 1 + assert summary.xy_circle_equivalence["area_fraction"][0] == 2 + assert summary.xy_circle_equivalence["diameter_um"][0] == 3 + assert summary.xy_circle_equivalence["orientation_angle"][0] == math.degrees(4) + assert summary.xz_circle_equivalence["grain_number"][0] == 5 + assert summary.xz_circle_equivalence["area_fraction"][0] == 6 + assert summary.xz_circle_equivalence["diameter_um"][0] == 7 + assert summary.xz_circle_equivalence["orientation_angle"][0] == math.degrees(8) + assert summary.yz_circle_equivalence["grain_number"][0] == 9 + assert summary.yz_circle_equivalence["area_fraction"][0] == 10 + assert summary.yz_circle_equivalence["diameter_um"][0] == 11 + assert summary.yz_circle_equivalence["orientation_angle"][0] == math.degrees(12) + assert summary.xy_average_grain_size == 6 + assert summary.xz_average_grain_size == 42 + assert summary.yz_average_grain_size == 110 + + # clean up + shutil.rmtree(user_data_path) + + +@pytest.mark.parametrize( + "invalid_obj", + [ + int(1), + None, + Microstructure3DResult(), + ], +) +def test_Microstructure3DSummary_init_raises_exception_for_invalid_input_type( + invalid_obj, +): + # arrange, act, assert + with pytest.raises(ValueError, match="Invalid input type"): + Microstructure3DSummary(invalid_obj, Microstructure3DResult(), ".") + + +@pytest.mark.parametrize( + "invalid_obj", + [ + int(1), + None, + Microstructure3DInput(), + ], +) +def test_Microstructure3DSummary_init_raises_exception_for_invalid_result_type( + invalid_obj, +): + # arrange, act, assert + with pytest.raises(ValueError, match="Invalid result type"): + Microstructure3DSummary(Microstructure3DInput(), invalid_obj, ".") + + +@pytest.mark.parametrize( + "invalid_path", + [ + "", + None, + ], +) +def test_Microstructure3DSummary_init_raises_exception_for_invalid_path( + invalid_path, +): + # arrange, act, assert + with pytest.raises(ValueError, match="Invalid user data path"): + Microstructure3DSummary(Microstructure3DInput(), Microstructure3DResult(), invalid_path) + + +def test_Microstructure3DSummary_repr_returns_expected_string(): + # arrange + user_data_path = os.path.join(tempfile.gettempdir(), "microstructure_3d_summary_init") + os.makedirs(user_data_path, exist_ok=True) + input = Microstructure3DInput(id="myId") + xy_vtk_bytes = bytes(range(3)) + xz_vtk_bytes = bytes(range(4, 6)) + yz_vtk_bytes = bytes(range(7, 9)) + grain_3d_bytes = bytes(range(10, 12)) + xy_stats = GrainStatistics(grain_number=1, area_fraction=2, diameter_um=3, orientation_angle=4) + xz_stats = GrainStatistics(grain_number=5, area_fraction=6, diameter_um=7, orientation_angle=8) + yz_stats = GrainStatistics( + grain_number=9, area_fraction=10, diameter_um=11, orientation_angle=12 + ) + result = MicrostructureResult(xy_vtk=xy_vtk_bytes, xz_vtk=xz_vtk_bytes, yz_vtk=yz_vtk_bytes) + result.xy_circle_equivalence.append(xy_stats) + result.xz_circle_equivalence.append(xz_stats) + result.yz_circle_equivalence.append(yz_stats) + result_3d = Microstructure3DResult(three_d_vtk=grain_3d_bytes, two_d_result=result) + summary = Microstructure3DSummary(input=input, result=result_3d, user_data_path=user_data_path) + expected_output_dir = os.path.join(user_data_path, "myId") + + # act, assert + assert repr(summary) == ( + "Microstructure3DSummary\n" + + "input: Microstructure3DInput\n" + + "id: myId\n" + + "sample_min_x: 0\n" + + "sample_min_y: 0\n" + + "sample_min_z: 0\n" + + "sample_size_x: 0.0001\n" + + "sample_size_y: 0.0001\n" + + "sample_size_z: 0.0001\n" + + "calculate_initial_microstructure: True\n" + + "use_transient_bulk_nucleation: True\n" + + "max_bulk_nucleation_density: 20000000000000\n" + + "num_initial_random_nuclei: 8000\n" + + "\n" + + "machine: AdditiveMachine\n" + + "laser_power: 195 W\n" + + "scan_speed: 1.0 m/s\n" + + "heater_temperature: 80 °C\n" + + "layer_thickness: 5e-05 m\n" + + "beam_diameter: 0.0001 m\n" + + "starting_layer_angle: 57 °\n" + + "layer_rotation_angle: 67 °\n" + + "hatch_spacing: 0.0001 m\n" + + "slicing_stripe_width: 0.01 m\n" + + "\n" + + "material: AdditiveMaterial\n" + + "absorptivity_maximum: 0\n" + + "absorptivity_minimum: 0\n" + + "absorptivity_powder_coefficient_a: 0\n" + + "absorptivity_powder_coefficient_b: 0\n" + + "absorptivity_solid_coefficient_a: 0\n" + + "absorptivity_solid_coefficient_b: 0\n" + + "anisotropic_strain_coefficient_parallel: 0\n" + + "anisotropic_strain_coefficient_perpendicular: 0\n" + + "anisotropic_strain_coefficient_z: 0\n" + + "elastic_modulus: 0\n" + + "hardening_factor: 0\n" + + "liquidus_temperature: 0\n" + + "material_yield_strength: 0\n" + + "name: \n" + + "nucleation_constant_bulk: 0\n" + + "nucleation_constant_interface: 0\n" + + "penetration_depth_maximum: 0\n" + + "penetration_depth_minimum: 0\n" + + "penetration_depth_powder_coefficient_a: 0\n" + + "penetration_depth_powder_coefficient_b: 0\n" + + "penetration_depth_solid_coefficient_a: 0\n" + + "penetration_depth_solid_coefficient_b: 0\n" + + "poisson_ratio: 0\n" + + "powder_packing_density: 0\n" + + "purging_gas_convection_coefficient: 0\n" + + "solid_density_at_room_temperature: 0\n" + + "solid_specific_heat_at_room_temperature: 0\n" + + "solid_thermal_conductivity_at_room_temperature: 0\n" + + "solidus_temperature: 0\n" + + "strain_scaling_factor: 0\n" + + "support_yield_strength_ratio: 0\n" + + "thermal_expansion_coefficient: 0\n" + + "vaporization_temperature: 0\n" + + "characteristic_width_data: CharacteristicWidthDataPoint[]\n" + + "thermal_properties_data: ThermalPropertiesDataPoint[]\n" + + "\n" + + "grain_3d_vtk: " + + os.path.join(expected_output_dir, "3d_grain_structure.vtk") + + "\n" + + "xy_vtk: " + + os.path.join(expected_output_dir, "xy.vtk") + + "\n" + + "xz_vtk: " + + os.path.join(expected_output_dir, "xz.vtk") + + "\n" + + "yz_vtk: " + + os.path.join(expected_output_dir, "yz.vtk") + + "\n" + + "xy_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + + "0 1 2.0 3.0 229.183118\n" + + "xz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + + "0 5 6.0 7.0 458.366236\n" + + "yz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + + "0 9 10.0 11.0 687.549354\n" + + "xy_average_grain_size: 6.0\n" + + "xz_average_grain_size: 42.0\n" + + "yz_average_grain_size: 110.0\n" + ) + + # cleanup + shutil.rmtree(user_data_path) + + +def test_Microstructure3DInput_init_creates_default_object(): + # arrange, act + input = Microstructure3DInput() + + # assert + assert input.id == "" + assert input.machine.laser_power == 195 + assert input.material.name == "" + assert input.sample_min_x == 0 + assert input.sample_min_y == 0 + assert input.sample_min_z == 0 + assert input.sample_size_x == 0.1e-3 + assert input.sample_size_y == 0.1e-3 + assert input.sample_size_z == 0.1e-3 + assert input.calculate_initial_microstructure is True + assert input.use_transient_bulk_nucleation is True + assert input.max_bulk_nucleation_density == 20e12 + assert input.num_initial_random_nuclei == 8000 + + +def test_Microstructure3DInput_init_with_parameters_creates_expected_object(): + # arrange + machine = AdditiveMachine() + machine.laser_power = 99 + material = AdditiveMaterial(name="vibranium") + + # act + input = Microstructure3DInput( + id="myId", + machine=machine, + material=material, + sample_min_x=1, + sample_min_y=2, + sample_min_z=3, + sample_size_x=0.0001, + sample_size_y=0.0002, + sample_size_z=0.0003, + use_transient_bulk_nucleation=True, + max_bulk_nucleation_density=8e6, + num_initial_random_nuclei=101, + ) + + # assert + assert "myId" == input.id + assert input.machine.laser_power == 99 + assert input.material.name == "vibranium" + assert input.sample_min_x == 1 + assert input.sample_min_y == 2 + assert input.sample_min_z == 3 + assert input.sample_size_x == 0.0001 + assert input.sample_size_y == 0.0002 + assert input.sample_size_z == 0.0003 + assert input.use_transient_bulk_nucleation is True + assert input.max_bulk_nucleation_density == 8e6 + assert input.num_initial_random_nuclei == 101 + + +def test_Microstructure3DInput_to_simulation_request_returns_expected_object(): + # arrange + input = Microstructure3DInput(id="myId") + + # act + request = input._to_simulation_request() + + # assert + assert isinstance(request, SimulationRequest) + assert request.id == "myId" + ms_input = request.microstructure_3d_input + assert ms_input.x_origin == 0 + assert ms_input.y_origin == 0 + assert ms_input.z_origin == 0 + assert ms_input.x_length == 0.1e-3 + assert ms_input.y_length == 0.1e-3 + assert ms_input.z_length == 0.1e-3 + assert ms_input.first_deposit_layer == 0 + assert ms_input.run_initial_microstructure is True + assert ms_input.num_random_nuclei == 8000 + assert ms_input.use_transient_bulk_nucleation is True + assert ms_input.max_bulk_nucleation_density == 20e12 + + +def test_Microstructure3DInput_to_simulation_request_assigns_values(): + # arrange + machine = AdditiveMachine() + machine.laser_power = 99 + material = AdditiveMaterial(name="vibranium") + input = Microstructure3DInput( + id="myId", + machine=machine, + material=material, + sample_min_x=1, + sample_min_y=2, + sample_min_z=3, + sample_size_x=0.0001, + sample_size_y=0.0002, + sample_size_z=0.0003, + calculate_initial_microstructure=False, + num_initial_random_nuclei=99, + use_transient_bulk_nucleation=False, + max_bulk_nucleation_density=999, + ) + + # act + request = input._to_simulation_request() + + # assert + assert isinstance(request, SimulationRequest) + assert request.id == "myId" + ms_input = request.microstructure_3d_input + assert ms_input.machine.laser_power == 99 + assert ms_input.material.name == "vibranium" + assert ms_input.x_origin == 1 + assert ms_input.y_origin == 2 + assert ms_input.z_origin == 3 + assert ms_input.x_length == 0.1e-3 + assert ms_input.y_length == 0.2e-3 + assert ms_input.z_length == 0.3e-3 + assert ms_input.first_deposit_layer == 0 + assert ms_input.run_initial_microstructure is False + assert ms_input.num_random_nuclei == 99 + assert ms_input.use_transient_bulk_nucleation is False + assert ms_input.max_bulk_nucleation_density == 999 + + +def test_Microstructure3DInput_setters_raise_ValueError_for_values_out_of_range(): + # arrange + input = Microstructure3DInput() + + # act & assert + with pytest.raises(ValueError): + input.sample_min_x = -1 + with pytest.raises(ValueError): + input.sample_min_x = 11 + with pytest.raises(ValueError): + input.sample_min_y = -1 + with pytest.raises(ValueError): + input.sample_min_y = 11 + with pytest.raises(ValueError): + input.sample_min_z = -1 + with pytest.raises(ValueError): + input.sample_min_z = 11 + with pytest.raises(ValueError): + input.sample_size_x = 14e-6 + with pytest.raises(ValueError): + input.sample_size_x = 6e-4 + with pytest.raises(ValueError): + input.sample_size_y = 14e-6 + with pytest.raises(ValueError): + input.sample_size_y = 6e-4 + with pytest.raises(ValueError): + input.sample_size_z = 14e-6 + with pytest.raises(ValueError): + input.sample_size_z = 6e-4 + with pytest.raises(ValueError): + input.max_bulk_nucleation_density = -1 + with pytest.raises(ValueError): + input.num_initial_random_nuclei = -1 + + +@pytest.mark.parametrize( + "field", + [ + "sample_min_x", + "sample_min_y", + "sample_min_z", + "sample_size_x", + "sample_size_y", + "sample_size_z", + "max_bulk_nucleation_density", + "num_initial_random_nuclei", + ], +) +def test_Microstructure3DInput_setters_raise_ValueError_for_nan_values(field): + # arrange + input = Microstructure3DInput() + + # act & assert + with pytest.raises(ValueError, match=field + " must be a number"): + setattr(input, field, float("nan")) + + +def test_Microstructure3DInput_repr_returns_expected_string(): + # arrange + input = Microstructure3DInput(id="myId") + + # act, assert + assert repr(input) == ( + "Microstructure3DInput\n" + + "id: myId\n" + + "sample_min_x: 0\n" + + "sample_min_y: 0\n" + + "sample_min_z: 0\n" + + "sample_size_x: 0.0001\n" + + "sample_size_y: 0.0001\n" + + "sample_size_z: 0.0001\n" + + "calculate_initial_microstructure: True\n" + + "use_transient_bulk_nucleation: True\n" + + "max_bulk_nucleation_density: 20000000000000\n" + + "num_initial_random_nuclei: 8000\n" + + "\n" + + "machine: AdditiveMachine\n" + + "laser_power: 195 W\n" + + "scan_speed: 1.0 m/s\n" + + "heater_temperature: 80 °C\n" + + "layer_thickness: 5e-05 m\n" + + "beam_diameter: 0.0001 m\n" + + "starting_layer_angle: 57 °\n" + + "layer_rotation_angle: 67 °\n" + + "hatch_spacing: 0.0001 m\n" + + "slicing_stripe_width: 0.01 m\n" + + "\n" + + "material: AdditiveMaterial\n" + + "absorptivity_maximum: 0\n" + + "absorptivity_minimum: 0\n" + + "absorptivity_powder_coefficient_a: 0\n" + + "absorptivity_powder_coefficient_b: 0\n" + + "absorptivity_solid_coefficient_a: 0\n" + + "absorptivity_solid_coefficient_b: 0\n" + + "anisotropic_strain_coefficient_parallel: 0\n" + + "anisotropic_strain_coefficient_perpendicular: 0\n" + + "anisotropic_strain_coefficient_z: 0\n" + + "elastic_modulus: 0\n" + + "hardening_factor: 0\n" + + "liquidus_temperature: 0\n" + + "material_yield_strength: 0\n" + + "name: \n" + + "nucleation_constant_bulk: 0\n" + + "nucleation_constant_interface: 0\n" + + "penetration_depth_maximum: 0\n" + + "penetration_depth_minimum: 0\n" + + "penetration_depth_powder_coefficient_a: 0\n" + + "penetration_depth_powder_coefficient_b: 0\n" + + "penetration_depth_solid_coefficient_a: 0\n" + + "penetration_depth_solid_coefficient_b: 0\n" + + "poisson_ratio: 0\n" + + "powder_packing_density: 0\n" + + "purging_gas_convection_coefficient: 0\n" + + "solid_density_at_room_temperature: 0\n" + + "solid_specific_heat_at_room_temperature: 0\n" + + "solid_thermal_conductivity_at_room_temperature: 0\n" + + "solidus_temperature: 0\n" + + "strain_scaling_factor: 0\n" + + "support_yield_strength_ratio: 0\n" + + "thermal_expansion_coefficient: 0\n" + + "vaporization_temperature: 0\n" + + "characteristic_width_data: CharacteristicWidthDataPoint[]\n" + + "thermal_properties_data: ThermalPropertiesDataPoint[]\n" + ) From 289fc4eb28e5aeed037fd6b892c5c0e6e717e466 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Thu, 29 Feb 2024 15:36:35 -0700 Subject: [PATCH 2/7] Remove 2d results from 3d micro summary --- ...re.py => 02_additive_2d_microstructure.py} | 9 +- examples/05_additive_3d_microstructure.py | 117 ++++++++++++++++++ src/ansys/additive/core/microstructure.py | 6 +- src/ansys/additive/core/microstructure_3d.py | 78 +----------- tests/test_microstructure_3d.py | 81 ++++++------ 5 files changed, 170 insertions(+), 121 deletions(-) rename examples/{02_additive_microstructure.py => 02_additive_2d_microstructure.py} (98%) create mode 100644 examples/05_additive_3d_microstructure.py diff --git a/examples/02_additive_microstructure.py b/examples/02_additive_2d_microstructure.py similarity index 98% rename from examples/02_additive_microstructure.py rename to examples/02_additive_2d_microstructure.py index 8606ff96b..b4a686722 100644 --- a/examples/02_additive_microstructure.py +++ b/examples/02_additive_2d_microstructure.py @@ -20,12 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ -Microstructure analysis -####################### +2D Microstructure analysis +########################## This example shows how to use PyAdditive to determine -the microstructure for a sample coupon with given material -and machine parameters. +the two-dimensional microstructure in the XY, XZ and YZ +planes for a sample coupon with given material and +machine parameters. Units are SI (m, kg, s, K) unless otherwise noted. """ diff --git a/examples/05_additive_3d_microstructure.py b/examples/05_additive_3d_microstructure.py new file mode 100644 index 000000000..a80c74b4e --- /dev/null +++ b/examples/05_additive_3d_microstructure.py @@ -0,0 +1,117 @@ +# 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. +""" +3D Microstructure analysis +########################### + +This example shows how to use PyAdditive to determine +the three-dimensional microstructure for a sample coupon +with given material and machine parameters. + +Units are SI (m, kg, s, K) unless otherwise noted. +""" +############################################################################### +# Perform required import and connect +# ----------------------------------- +# Perform the required import and connect to the Additive service. + +import pyvista as pv + +from ansys.additive.core import Additive, AdditiveMachine, Microstructure3DInput, SimulationError + +additive = Additive() + +############################################################################### +# Select material +# --------------- +# Select a material. You can use the :meth:`~Additive.materials_list` method to +# obtain a list of available materials. + +print("Available material names: {}".format(additive.materials_list())) + +############################################################################### +# You can obtain the parameters for a single material by passing a name +# from the materials list to the :meth:`~Additive.material` method. + +material = additive.material("17-4PH") + +############################################################################### +# Specify machine parameters +# -------------------------- +# Specify machine parameters by first creating an :class:`AdditiveMachine` object +# and then assigning the desired values. All values are in SI units (m, kg, s, K) +# unless otherwise noted. + +machine = AdditiveMachine() + +# Show available parameters +print(machine) + +############################################################################### +# Set laser power and scan speed +# ------------------------------ +# Set the laser power and scan speed. + +machine.scan_speed = 1 # m/s +machine.laser_power = 500 # W + +############################################################################### +# Specify inputs for 3D microstructure simulation +# ------------------------------------------------ +# Specify microstructure inputs. +input = Microstructure3DInput( + machine=machine, + material=material, + id="micro-3d", + sample_size_x=0.0001, # in meters (.1 mm) + sample_size_y=0.0001, + sample_size_z=0.0001, +) + +############################################################################### +# Run simulation +# -------------- +# Use the :meth:`~Additive.simulate` method of the ``additive`` object to run the simulation. +# The returned object is either a :class:`Microstructure3DSummary` object or a +# :class:`SimulationError` object. + +summary = additive.simulate(input) +if isinstance(summary, SimulationError): + raise Exception(summary.message) + + +############################################################################### +# Plot results +# ------------ +# The ``summary``` object includes a VTK file describing the 3D grain structure. +# The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, +# ``Phi1``, ``Phi2`` and ``Temperatures``. + +# Plot the Phi0 data of the 3D grain structure +cmap = "coolwarm" +ms3d = pv.read(summary.grain_3d_vtk) +ms3d.plot(scalars="Phi0", cmap=cmap) + +# Add a cutting plane to the plot +plotter = pv.Plotter() +plotter.add_mesh_clip_plane(ms3d, scalars="Phi0", cmap=cmap) +plotter.show() diff --git a/src/ansys/additive/core/microstructure.py b/src/ansys/additive/core/microstructure.py index f8929a592..704377184 100644 --- a/src/ansys/additive/core/microstructure.py +++ b/src/ansys/additive/core/microstructure.py @@ -540,7 +540,7 @@ def input(self): @property def xy_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XY plane. + """Path to the VTK file containing the 2D grain structure data in the XY plane. The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and `GrainNumber`. @@ -549,7 +549,7 @@ def xy_vtk(self) -> str: @property def xz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XZ plane. + """Path to the VTK file containing the 2D grain structure data in the XZ plane. The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and `GrainNumber`. @@ -558,7 +558,7 @@ def xz_vtk(self) -> str: @property def yz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the YZ plane. + """Path to the VTK file containing the 2D grain structure data in the YZ plane. The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and `GrainNumber`. diff --git a/src/ansys/additive/core/microstructure_3d.py b/src/ansys/additive/core/microstructure_3d.py index 94eb9db01..349543f04 100644 --- a/src/ansys/additive/core/microstructure_3d.py +++ b/src/ansys/additive/core/microstructure_3d.py @@ -28,12 +28,10 @@ ) from ansys.api.additive.v0.additive_domain_pb2 import Microstructure3DResult from ansys.api.additive.v0.additive_simulation_pb2 import SimulationRequest -import pandas as pd from ansys.additive.core import misc from ansys.additive.core.machine import AdditiveMachine from ansys.additive.core.material import AdditiveMaterial -from ansys.additive.core.microstructure import _Microstructure2DResult class Microstructure3DInput: @@ -351,16 +349,14 @@ def __init__( id = input.id if input.id else misc.short_uuid() outpath = os.path.join(user_data_path, id) os.makedirs(outpath, exist_ok=True) - self._2d_result = _Microstructure2DResult(result.two_d_result, outpath) self._grain_3d_vtk = os.path.join(outpath, self._3D_GRAIN_VTK_NAME) with open(self._grain_3d_vtk, "wb") as f: f.write(result.three_d_vtk) def __repr__(self): repr = type(self).__name__ + "\n" - for k in [x for x in self.__dict__ if x != "_2d_result"]: + for k in self.__dict__: repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" - repr += self._2d_result.__repr__() return repr @property @@ -373,75 +369,9 @@ def input(self): @property def grain_3d_vtk(self) -> str: - """Path to the VTK file containing the 3-D grain structure data. + """Path to the VTK file containing the 3D grain structure data. - The VTK file contains scalar data sets `GrainNumber`, `Phi0`, - `Phi1`, `Phi2` and `Temperatures`. + The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, + ``Phi1``, ``Phi2`` and ``Temperatures``. """ return self._grain_3d_vtk - - @property - def xy_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XY plane. - - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. - """ - return self._2d_result._xy_vtk - - @property - def xz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the XZ plane. - - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. - """ - return self._2d_result._xz_vtk - - @property - def yz_vtk(self) -> str: - """Path to the VTK file containing the 2-D grain structure data in the YZ plane. - - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. - """ - return self._2d_result._yz_vtk - - @property - def xy_circle_equivalence(self) -> pd.DataFrame: - """Circle equivalence data for the XY plane. - - For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. - """ - return self._2d_result._xy_circle_equivalence - - @property - def xz_circle_equivalence(self) -> pd.DataFrame: - """Circle equivalence data for the XZ plane. - - For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. - """ - return self._2d_result._xz_circle_equivalence - - @property - def yz_circle_equivalence(self) -> pd.DataFrame: - """Circle equivalence data for the YZ plane. - - For data frame column names, see the :class:`CircleEquivalenceColumnNames` class. - """ - return self._2d_result._yz_circle_equivalence - - @property - def xy_average_grain_size(self) -> float: - """Average grain size (µm) for the XY plane.""" - return self._2d_result._xy_average_grain_size - - @property - def xz_average_grain_size(self) -> float: - """Average grain size (µm) for the XZ plane.""" - return self._2d_result._xz_average_grain_size - - @property - def yz_average_grain_size(self) -> float: - """Average grain size (µm) for the YZ plane.""" - return self._2d_result._yz_average_grain_size diff --git a/tests/test_microstructure_3d.py b/tests/test_microstructure_3d.py index 3ee242e20..2691cd2ec 100644 --- a/tests/test_microstructure_3d.py +++ b/tests/test_microstructure_3d.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import math import os import shutil import tempfile @@ -65,27 +64,28 @@ def test_Microstructure3DSummary_init_returns_expected_value(): assert isinstance(summary, Microstructure3DSummary) assert input == summary.input assert summary.grain_3d_vtk == os.path.join(user_data_path, "id", summary._3D_GRAIN_VTK_NAME) - assert summary.xy_vtk == os.path.join(user_data_path, "id", "xy.vtk") - assert os.path.exists(summary.xy_vtk) - assert summary.xz_vtk == os.path.join(user_data_path, "id", "xz.vtk") - assert os.path.exists(summary.xz_vtk) - assert summary.yz_vtk == os.path.join(user_data_path, "id", "yz.vtk") - assert os.path.exists(summary.yz_vtk) - assert summary.xy_circle_equivalence["grain_number"][0] == 1 - assert summary.xy_circle_equivalence["area_fraction"][0] == 2 - assert summary.xy_circle_equivalence["diameter_um"][0] == 3 - assert summary.xy_circle_equivalence["orientation_angle"][0] == math.degrees(4) - assert summary.xz_circle_equivalence["grain_number"][0] == 5 - assert summary.xz_circle_equivalence["area_fraction"][0] == 6 - assert summary.xz_circle_equivalence["diameter_um"][0] == 7 - assert summary.xz_circle_equivalence["orientation_angle"][0] == math.degrees(8) - assert summary.yz_circle_equivalence["grain_number"][0] == 9 - assert summary.yz_circle_equivalence["area_fraction"][0] == 10 - assert summary.yz_circle_equivalence["diameter_um"][0] == 11 - assert summary.yz_circle_equivalence["orientation_angle"][0] == math.degrees(12) - assert summary.xy_average_grain_size == 6 - assert summary.xz_average_grain_size == 42 - assert summary.yz_average_grain_size == 110 + # TODO: uncomment when the following properties are implemented + # assert summary.xy_vtk == os.path.join(user_data_path, "id", "xy.vtk") + # assert os.path.exists(summary.xy_vtk) + # assert summary.xz_vtk == os.path.join(user_data_path, "id", "xz.vtk") + # assert os.path.exists(summary.xz_vtk) + # assert summary.yz_vtk == os.path.join(user_data_path, "id", "yz.vtk") + # assert os.path.exists(summary.yz_vtk) + # assert summary.xy_circle_equivalence["grain_number"][0] == 1 + # assert summary.xy_circle_equivalence["area_fraction"][0] == 2 + # assert summary.xy_circle_equivalence["diameter_um"][0] == 3 + # assert summary.xy_circle_equivalence["orientation_angle"][0] == math.degrees(4) + # assert summary.xz_circle_equivalence["grain_number"][0] == 5 + # assert summary.xz_circle_equivalence["area_fraction"][0] == 6 + # assert summary.xz_circle_equivalence["diameter_um"][0] == 7 + # assert summary.xz_circle_equivalence["orientation_angle"][0] == math.degrees(8) + # assert summary.yz_circle_equivalence["grain_number"][0] == 9 + # assert summary.yz_circle_equivalence["area_fraction"][0] == 10 + # assert summary.yz_circle_equivalence["diameter_um"][0] == 11 + # assert summary.yz_circle_equivalence["orientation_angle"][0] == math.degrees(12) + # assert summary.xy_average_grain_size == 6 + # assert summary.xz_average_grain_size == 42 + # assert summary.yz_average_grain_size == 110 # clean up shutil.rmtree(user_data_path) @@ -227,24 +227,25 @@ def test_Microstructure3DSummary_repr_returns_expected_string(): + "grain_3d_vtk: " + os.path.join(expected_output_dir, "3d_grain_structure.vtk") + "\n" - + "xy_vtk: " - + os.path.join(expected_output_dir, "xy.vtk") - + "\n" - + "xz_vtk: " - + os.path.join(expected_output_dir, "xz.vtk") - + "\n" - + "yz_vtk: " - + os.path.join(expected_output_dir, "yz.vtk") - + "\n" - + "xy_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" - + "0 1 2.0 3.0 229.183118\n" - + "xz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" - + "0 5 6.0 7.0 458.366236\n" - + "yz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" - + "0 9 10.0 11.0 687.549354\n" - + "xy_average_grain_size: 6.0\n" - + "xz_average_grain_size: 42.0\n" - + "yz_average_grain_size: 110.0\n" + # TODO: uncomment when the following properties are implemented + # + "xy_vtk: " + # + os.path.join(expected_output_dir, "xy.vtk") + # + "\n" + # + "xz_vtk: " + # + os.path.join(expected_output_dir, "xz.vtk") + # + "\n" + # + "yz_vtk: " + # + os.path.join(expected_output_dir, "yz.vtk") + # + "\n" + # + "xy_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + # + "0 1 2.0 3.0 229.183118\n" + # + "xz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + # + "0 5 6.0 7.0 458.366236\n" + # + "yz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" + # + "0 9 10.0 11.0 687.549354\n" + # + "xy_average_grain_size: 6.0\n" + # + "xz_average_grain_size: 42.0\n" + # + "yz_average_grain_size: 110.0\n" ) # cleanup From 61b10b16a9a72803f01d3494e31916fd0a6693d7 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Thu, 29 Feb 2024 15:45:29 -0700 Subject: [PATCH 3/7] Use latest version of server in build pipeline --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 459150f4e..37a200db8 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -18,7 +18,7 @@ env: DOCUMENTATION_CNAME: "additive.docs.pyansys.com" LIBRARY_NAME: "ansys-additive-core" # NOTE: The server needs to stay in a private registry. - ANSYS_PRODUCT_IMAGE: "ghcr.io/ansys-internal/additive:24.2.0-alpha1" + ANSYS_PRODUCT_IMAGE: "ghcr.io/ansys-internal/additive:latest" ANSYS_PRODUCT_CONTAINER: "ansys-additive-container" concurrency: From e7cf601b2f51361d1938bdd5795b017b70bb7714 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Thu, 29 Feb 2024 16:36:49 -0700 Subject: [PATCH 4/7] Add ave grain sizes to 3d micro summary --- examples/05_additive_3d_microstructure.py | 16 +++++++++++--- src/ansys/additive/core/microstructure_3d.py | 22 +++++++++++++++++++- tests/test_microstructure_3d.py | 12 +++++------ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/examples/05_additive_3d_microstructure.py b/examples/05_additive_3d_microstructure.py index a80c74b4e..827427988 100644 --- a/examples/05_additive_3d_microstructure.py +++ b/examples/05_additive_3d_microstructure.py @@ -100,8 +100,8 @@ ############################################################################### -# Plot results -# ------------ +# Plot 3D grain visualization +# --------------------------- # The ``summary``` object includes a VTK file describing the 3D grain structure. # The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, # ``Phi1``, ``Phi2`` and ``Temperatures``. @@ -111,7 +111,17 @@ ms3d = pv.read(summary.grain_3d_vtk) ms3d.plot(scalars="Phi0", cmap=cmap) -# Add a cutting plane to the plot +# Add a cut plane to the plot plotter = pv.Plotter() plotter.add_mesh_clip_plane(ms3d, scalars="Phi0", cmap=cmap) plotter.show() + +############################################################################### +# Print average grain sizes +# ------------------------- +# The ``summary``` object includes the average grain sizes in the XY, XZ, and YZ +# planes. + +print("Average grain size in XY plane: {} µm".format(summary.xy_average_grain_size)) +print("Average grain size in XZ plane: {} µm".format(summary.xz_average_grain_size)) +print("Average grain size in YZ plane: {} µm".format(summary.yz_average_grain_size)) diff --git a/src/ansys/additive/core/microstructure_3d.py b/src/ansys/additive/core/microstructure_3d.py index 349543f04..0a1d1b48d 100644 --- a/src/ansys/additive/core/microstructure_3d.py +++ b/src/ansys/additive/core/microstructure_3d.py @@ -32,6 +32,7 @@ from ansys.additive.core import misc from ansys.additive.core.machine import AdditiveMachine from ansys.additive.core.material import AdditiveMaterial +from ansys.additive.core.microstructure import _Microstructure2DResult class Microstructure3DInput: @@ -352,11 +353,15 @@ def __init__( self._grain_3d_vtk = os.path.join(outpath, self._3D_GRAIN_VTK_NAME) with open(self._grain_3d_vtk, "wb") as f: f.write(result.three_d_vtk) + self._2d_result = _Microstructure2DResult(result.two_d_result, outpath) def __repr__(self): repr = type(self).__name__ + "\n" - for k in self.__dict__: + for k in [x for x in self.__dict__ if x != "_2d_result"]: repr += k.replace("_", "", 1) + ": " + str(getattr(self, k)) + "\n" + repr += f"xy_average_grain_size: {self.xy_average_grain_size}\n" + repr += f"xz_average_grain_size: {self.xz_average_grain_size}\n" + repr += f"yz_average_grain_size: {self.yz_average_grain_size}\n" return repr @property @@ -375,3 +380,18 @@ def grain_3d_vtk(self) -> str: ``Phi1``, ``Phi2`` and ``Temperatures``. """ return self._grain_3d_vtk + + @property + def xy_average_grain_size(self) -> float: + """Average grain size (µm) for the XY plane.""" + return self._2d_result._xy_average_grain_size + + @property + def xz_average_grain_size(self) -> float: + """Average grain size (µm) for the XZ plane.""" + return self._2d_result._xz_average_grain_size + + @property + def yz_average_grain_size(self) -> float: + """Average grain size (µm) for the YZ plane.""" + return self._2d_result._yz_average_grain_size diff --git a/tests/test_microstructure_3d.py b/tests/test_microstructure_3d.py index 2691cd2ec..2e04ef1f3 100644 --- a/tests/test_microstructure_3d.py +++ b/tests/test_microstructure_3d.py @@ -64,6 +64,9 @@ def test_Microstructure3DSummary_init_returns_expected_value(): assert isinstance(summary, Microstructure3DSummary) assert input == summary.input assert summary.grain_3d_vtk == os.path.join(user_data_path, "id", summary._3D_GRAIN_VTK_NAME) + assert summary.xy_average_grain_size == 6 + assert summary.xz_average_grain_size == 42 + assert summary.yz_average_grain_size == 110 # TODO: uncomment when the following properties are implemented # assert summary.xy_vtk == os.path.join(user_data_path, "id", "xy.vtk") # assert os.path.exists(summary.xy_vtk) @@ -83,9 +86,6 @@ def test_Microstructure3DSummary_init_returns_expected_value(): # assert summary.yz_circle_equivalence["area_fraction"][0] == 10 # assert summary.yz_circle_equivalence["diameter_um"][0] == 11 # assert summary.yz_circle_equivalence["orientation_angle"][0] == math.degrees(12) - # assert summary.xy_average_grain_size == 6 - # assert summary.xz_average_grain_size == 42 - # assert summary.yz_average_grain_size == 110 # clean up shutil.rmtree(user_data_path) @@ -227,6 +227,9 @@ def test_Microstructure3DSummary_repr_returns_expected_string(): + "grain_3d_vtk: " + os.path.join(expected_output_dir, "3d_grain_structure.vtk") + "\n" + + "xy_average_grain_size: 6.0\n" + + "xz_average_grain_size: 42.0\n" + + "yz_average_grain_size: 110.0\n" # TODO: uncomment when the following properties are implemented # + "xy_vtk: " # + os.path.join(expected_output_dir, "xy.vtk") @@ -243,9 +246,6 @@ def test_Microstructure3DSummary_repr_returns_expected_string(): # + "0 5 6.0 7.0 458.366236\n" # + "yz_circle_equivalence: grain_number area_fraction diameter_um orientation_angle\n" # + "0 9 10.0 11.0 687.549354\n" - # + "xy_average_grain_size: 6.0\n" - # + "xz_average_grain_size: 42.0\n" - # + "yz_average_grain_size: 110.0\n" ) # cleanup From 70018d8c04a2d2486985ae86b9a778e18fbf2bc3 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Thu, 29 Feb 2024 16:48:51 -0700 Subject: [PATCH 5/7] Remove commented parameter --- src/ansys/additive/core/progress_logger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/additive/core/progress_logger.py b/src/ansys/additive/core/progress_logger.py index 6fdb27728..4f89f37f3 100644 --- a/src/ansys/additive/core/progress_logger.py +++ b/src/ansys/additive/core/progress_logger.py @@ -59,7 +59,6 @@ def log_progress(self, progress: Progress): total=100, colour="green", desc=self._last_context, - # mininterval=0.001, dynamic_ncols=True, ) From 1ea71ee46c09b772561c5709d5b29453a1903101 Mon Sep 17 00:00:00 2001 From: Peter Krull - ANSYS Date: Fri, 1 Mar 2024 15:59:31 -0700 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- examples/02_additive_2d_microstructure.py | 2 +- examples/05_additive_3d_microstructure.py | 2 +- src/ansys/additive/core/microstructure.py | 12 +++++------ src/ansys/additive/core/microstructure_3d.py | 22 ++++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/02_additive_2d_microstructure.py b/examples/02_additive_2d_microstructure.py index b4a686722..3a6a9e547 100644 --- a/examples/02_additive_2d_microstructure.py +++ b/examples/02_additive_2d_microstructure.py @@ -24,7 +24,7 @@ ########################## This example shows how to use PyAdditive to determine -the two-dimensional microstructure in the XY, XZ and YZ +the two-dimensional microstructure in the XY, XZ, and YZ planes for a sample coupon with given material and machine parameters. diff --git a/examples/05_additive_3d_microstructure.py b/examples/05_additive_3d_microstructure.py index 827427988..550b3c481 100644 --- a/examples/05_additive_3d_microstructure.py +++ b/examples/05_additive_3d_microstructure.py @@ -104,7 +104,7 @@ # --------------------------- # The ``summary``` object includes a VTK file describing the 3D grain structure. # The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, -# ``Phi1``, ``Phi2`` and ``Temperatures``. +# ``Phi1``, ``Phi2``, and ``Temperatures``. # Plot the Phi0 data of the 3D grain structure cmap = "coolwarm" diff --git a/src/ansys/additive/core/microstructure.py b/src/ansys/additive/core/microstructure.py index 704377184..2eb1d5775 100644 --- a/src/ansys/additive/core/microstructure.py +++ b/src/ansys/additive/core/microstructure.py @@ -542,8 +542,8 @@ def input(self): def xy_vtk(self) -> str: """Path to the VTK file containing the 2D grain structure data in the XY plane. - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. + The VTK file contains these scalar data sets: ``GrainBoundaries``, ``Orientation_(deg)``, and + ``GrainNumber``. """ return self._result._xy_vtk @@ -551,8 +551,8 @@ def xy_vtk(self) -> str: def xz_vtk(self) -> str: """Path to the VTK file containing the 2D grain structure data in the XZ plane. - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. + The VTK file contains these scalar data sets: ``GrainBoundaries``, + ``Orientation_(deg)``, and ``GrainNumber``. """ return self._result._xz_vtk @@ -560,8 +560,8 @@ def xz_vtk(self) -> str: def yz_vtk(self) -> str: """Path to the VTK file containing the 2D grain structure data in the YZ plane. - The VTK file contains scalar data sets `GrainBoundaries`, `Orientation_(deg)` and - `GrainNumber`. + The VTK file contains these scalar data sets: ``GrainBoundaries``, + ``Orientation_(deg)``, and ``GrainNumber``. """ return self._result._yz_vtk diff --git a/src/ansys/additive/core/microstructure_3d.py b/src/ansys/additive/core/microstructure_3d.py index 0a1d1b48d..15f26c726 100644 --- a/src/ansys/additive/core/microstructure_3d.py +++ b/src/ansys/additive/core/microstructure_3d.py @@ -139,7 +139,7 @@ def __validate_greater_than_zero(value, name): @property def id(self) -> str: - """User-provided ID for this simulation.""" + """User-provided ID for the simulation.""" return self._id @id.setter @@ -256,11 +256,11 @@ def sample_size_z(self, value: float): @property def calculate_initial_microstructure(self) -> bool: - """Whether to run the initial microstructure solver. + """Flag indicating if the initial microstructure solver is to run. - If ``True``, the initial microstructure solver will be used to create initial - condition grain identifiers and euler angles. - If ``False``, the initial microstructure solver will not be run. + If ``True``, the initial microstructure solver is used to create initial + condition grain identifiers and Euler angles. + If ``False``, the initial microstructure solver is not be run. """ return self._calculate_initial_microstructure @@ -270,7 +270,7 @@ def calculate_initial_microstructure(self, value: bool): @property def use_transient_bulk_nucleation(self) -> bool: - """Allow nucleation in the bulk region of the meltpool. + """Flag indicating if nucleation is allowed in the bulk region of the meltpool. Nucleation rate is controlled by bulk nucleation density. If ``True``, bulk nucleation is enabled. if ``False``, bulk @@ -286,7 +286,7 @@ def use_transient_bulk_nucleation(self, value: bool): def max_bulk_nucleation_density(self) -> int: """Maximum nucleation density in the bulk (grains/m^3). - If ``use_transient_bulk_nucleation`` is ``False``, this value is ignored. + If ``use_transient_bulk_nucleation=False``, this value is ignored. """ return self._max_bulk_nucleation_density @@ -299,8 +299,8 @@ def max_bulk_nucleation_density(self, value: int): def num_initial_random_nuclei(self) -> int: """Number of random nuclei to use for the microstructure initial conditions. - This value will be used by the initial microstructure conditions solver. - If ``use_transient_bulk_nucleation`` is ``True``, this value is ignored. + This value is used by the initial microstructure conditions solver. + If ``use_transient_bulk_nucleation=True``, this value is ignored. """ return self._num_initial_random_nuclei @@ -376,8 +376,8 @@ def input(self): def grain_3d_vtk(self) -> str: """Path to the VTK file containing the 3D grain structure data. - The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, - ``Phi1``, ``Phi2`` and ``Temperatures``. + The VTK file contains these scalar data sets" ``GrainNumber``, ``Phi0``, + ``Phi1``, ``Phi2``, and ``Temperatures``. """ return self._grain_3d_vtk From 5b96f6be4c7ac1fde03d18ceb2024bfc1c6d53f6 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Mon, 4 Mar 2024 09:19:03 -0700 Subject: [PATCH 7/7] Add beta disclaimer --- ... => 12_additive_3d_microstructure_beta.py} | 30 ++++++++++++++++--- src/ansys/additive/core/microstructure_3d.py | 7 ++--- 2 files changed, 29 insertions(+), 8 deletions(-) rename examples/{05_additive_3d_microstructure.py => 12_additive_3d_microstructure_beta.py} (75%) diff --git a/examples/05_additive_3d_microstructure.py b/examples/12_additive_3d_microstructure_beta.py similarity index 75% rename from examples/05_additive_3d_microstructure.py rename to examples/12_additive_3d_microstructure_beta.py index 550b3c481..f485c1104 100644 --- a/examples/05_additive_3d_microstructure.py +++ b/examples/12_additive_3d_microstructure_beta.py @@ -20,8 +20,30 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ -3D Microstructure analysis -########################### +3D Microstructure analysis (BETA) +################################# + +.. warning:: + Beta Features Disclaimer + + * This is beta documentation for one or more beta software features. + * Beta features are considered unreleased and have not been fully tested nor + fully validated. The results are not guaranteed by Ansys, Inc. (Ansys) to be + correct. You assume the risk of using beta features. + * At its discretion, Ansys may release, change, or withdraw beta features + in future revisions. + * Beta features are not subject to the Ansys Class 3 error reporting system. + Ansys makes no commitment to resolve defects reported against beta features; + however, your feedback will help us improve the quality of the product. + * Ansys does not guarantee that database and/or input files used with beta + features will run successfully from version to version of the software, nor + with the final released version of the features. You may need to modify the + database and/or input files before running them on other versions. + * Documentation for beta features is called beta documentation, and it may + not be written to the same standard as documentation for released features. + Beta documentation may not be complete at the time of product release. + At its discretion, Ansys may add, change, or delete beta documentation + at any time. This example shows how to use PyAdditive to determine the three-dimensional microstructure for a sample coupon @@ -102,7 +124,7 @@ ############################################################################### # Plot 3D grain visualization # --------------------------- -# The ``summary``` object includes a VTK file describing the 3D grain structure. +# The ``summary`` object includes a VTK file describing the 3D grain structure. # The VTK file contains scalar data sets ``GrainNumber``, ``Phi0``, # ``Phi1``, ``Phi2``, and ``Temperatures``. @@ -119,7 +141,7 @@ ############################################################################### # Print average grain sizes # ------------------------- -# The ``summary``` object includes the average grain sizes in the XY, XZ, and YZ +# The ``summary`` object includes the average grain sizes in the XY, XZ, and YZ # planes. print("Average grain size in XY plane: {} µm".format(summary.xy_average_grain_size)) diff --git a/src/ansys/additive/core/microstructure_3d.py b/src/ansys/additive/core/microstructure_3d.py index 15f26c726..3327e76a6 100644 --- a/src/ansys/additive/core/microstructure_3d.py +++ b/src/ansys/additive/core/microstructure_3d.py @@ -256,11 +256,10 @@ def sample_size_z(self, value: float): @property def calculate_initial_microstructure(self) -> bool: - """Flag indicating if the initial microstructure solver is to run. + """Flag indicating if the initial microstructure conditions solver is to run. - If ``True``, the initial microstructure solver is used to create initial - condition grain identifiers and Euler angles. - If ``False``, the initial microstructure solver is not be run. + If ``True``, initial condition grain identifiers and Euler angles are calculated. + If ``False``, the initial microstructure conditions solver is not be run. """ return self._calculate_initial_microstructure