Source code for turbo_turtle._cubit_python

"""Python 3 module that imports cubit

Which requires that Cubit's bin directory is found on PYTHONPATH, either directly by the end user or from a successful
:meth:`turbo_turtle._utilities.find_cubit_bin` call and internal ``sys.path`` modification. This module does *not*
perform ``sys.path`` manipulation, so the importing/calling module/script *must* verify that Cubit will import correctly
first.
"""

import shutil
import typing
import pathlib
import tempfile

import numpy

from turbo_turtle import _utilities
from turbo_turtle._abaqus_python.turbo_turtle_abaqus import _mixed_utilities
from turbo_turtle._abaqus_python.turbo_turtle_abaqus import vertices
from turbo_turtle._abaqus_python.turbo_turtle_abaqus import parsers


cubit = _utilities.import_cubit()


[docs] def cubit_command_or_exception(command): """Thin wrapper around ``cubit.cmd`` to raise an exception when returning False Cubit returns True/False on ``cubit.cmd("")`` calls, but does not raise an exception. This method will raise a RuntimeError when the command returns False. :param str command: Cubit APREPRO command to execute """ success = cubit.cmd(command) if not success: raise RuntimeError(f"Command '{command}' returned an error. Please see the Cubit log for details.") return success
[docs] def geometry( input_file, output_file, planar=parsers.geometry_defaults["planar"], part_name=parsers.geometry_defaults["part_name"], unit_conversion=parsers.geometry_defaults["unit_conversion"], euclidean_distance=parsers.geometry_defaults["euclidean_distance"], delimiter=parsers.geometry_defaults["delimiter"], header_lines=parsers.geometry_defaults["header_lines"], revolution_angle=parsers.geometry_defaults["revolution_angle"], y_offset=parsers.geometry_defaults["y_offset"], rtol=parsers.geometry_defaults["rtol"], atol=parsers.geometry_defaults["atol"], ): """Create 2D planar, 2D axisymmetric, or 3D revolved geometry from an array of XY coordinates. Note that 2D axisymmetric sketches and sketches for 3D bodies of revolution about the global Y-axis must lie entirely on the positive-X side of the global Y-axis. This function can create multiple sheet bodies or volumes in the same Cubit ``*.cub`` file. If no part (body/volume) names are provided, the body/volume will be named after the input file base name. :param str input_file: input text file(s) with coordinates to draw :param str output_file: Cubit ``*.cub`` database to save the part(s) :param bool planar: switch to indicate that 2D model dimensionality is planar, not axisymmetric :param list part_name: name(s) of the part(s) being created :param float unit_conversion: multiplication factor applies to all coordinates :param float euclidean_distance: if the distance between two coordinates is greater than this, draw a straight line. Distance should be provided in units *after* the unit conversion :param str delimiter: character to use as a delimiter when reading the input file :param int header_lines: number of lines in the header to skip when reading the input file :param float revolution_angle: angle of solid revolution for ``3D`` geometries. Ignore when planar is True. :param float y_offset: vertical offset along the global Y-axis. Offset should be provided in units *after* the unit conversion. :param float rtol: relative tolerance for vertical/horizontal line checks :param float atol: absolute tolerance for vertical/horizontal line checks :returns: writes ``{output_file}.cub`` """ # TODO: Figure out how to log the Cubit operations without printing to console # TODO: Figure out how to get a better log of the non-APREPRO actions cubit.init(["cubit"]) part_name = _mixed_utilities.validate_part_name(input_file, part_name) part_name = _mixed_utilities.cubit_part_names(part_name) output_file = pathlib.Path(output_file).with_suffix(".cub") surfaces = [] for file_name, new_part in zip(input_file, part_name): coordinates = _mixed_utilities.return_genfromtxt( file_name, delimiter, header_lines, expected_dimensions=2, expected_columns=2 ) coordinates = vertices.scale_and_offset_coordinates(coordinates, unit_conversion, y_offset) lines, splines = vertices.lines_and_splines(coordinates, euclidean_distance, rtol=rtol, atol=atol) surfaces.append(_draw_surface(lines, splines)) for surface, new_part in zip(surfaces, part_name): _rename_and_sweep(surface, new_part, planar=planar, revolution_angle=revolution_angle) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def _draw_surface(lines, splines): """Given ordered lists of line/spline coordinates, create a Cubit surface object :param list lines: list of [2, 2] shaped arrays of (x, y) coordinates defining a line segment :param list splines: list of [N, 2] shaped arrays of (x, y) coordinates defining a spline :returns: Cubit surface defined by the lines and splines input :rtype: cubit.Surface """ curves = [] for first, second in lines: point1 = tuple(first) + (0.0,) point2 = tuple(second) + (0.0,) curves.append(create_curve_from_coordinates(point1, point2)) for spline in splines: zero_column = numpy.zeros([len(spline), 1]) spline_3d = numpy.append(spline, zero_column, axis=1) curves.append(create_spline_from_coordinates(spline_3d)) return cubit.create_surface(curves)
[docs] def create_curve_from_coordinates(point1, point2): """Create a curve from 2 three-dimensional coordinates :param tuple point1: First set of coordinates (x1, y1, z1) :param tuple point2: Second set of coordinates (x2, y2, z2) :returns: Cubit curve object defining a line segment :rtype: cubit.Curve """ vertex1 = cubit.create_vertex(*tuple(point1)) vertex2 = cubit.create_vertex(*tuple(point2)) return cubit.create_curve(vertex1, vertex2)
[docs] def create_spline_from_coordinates(coordinates): """Create a spline from a list of coordinates :param numpy.array coordinates: [N, 3] array of coordinates (x, y, z) :returns: Cubit curve object defining a spline :rtype: cubit.Curve """ coordinates = numpy.array(coordinates) minimum = 2 if coordinates.shape[0] < minimum: raise RuntimeError(f"Requires at least {minimum} coordinates to create a spline") points = [] for point in coordinates: points.append(cubit.create_vertex(*tuple(point))) vertex_ids = [point.id() for point in points] vertex_ids_text = _utilities.character_delimited_list(vertex_ids) # TODO: Find a suitable Cubit Python function for creating splines that returns the curve object cubit_command_or_exception(f"create curve spline vertex {vertex_ids_text} delete") curve = points[0].curves()[0] return curve
[docs] def create_arc_from_coordinates(center, point1, point2): """Create a circular arc cubit.Curve object from center and points on the curve :param tuple center: tuple of floats (X, Y, Z) location for the center of the circle arc :param tuple point1: tuple of floats (X, Y, Z) location for the first point on the arc :param tuple point2: tuple of floats (X, Y, Z) location for the second point on the arc :returns: cubit curve object :rtype: curbit.Curve """ center_vertex = cubit.create_vertex(*tuple(center)) # Cubit creates arcs with anticlockwise rotation. Order vertices with most negative Y axis coordinate first. if point1[1] < point2[1]: vertex1 = cubit.create_vertex(*tuple(point1)) vertex2 = cubit.create_vertex(*tuple(point2)) else: vertex1 = cubit.create_vertex(*tuple(point2)) vertex2 = cubit.create_vertex(*tuple(point1)) # TODO: Find a suitable Cubit Python function for creating arcs that returns the curve object command = f"create curve arc center vertex {center_vertex.id()} {vertex1.id()} {vertex2.id()} normal 0 0 1" cubit_command_or_exception(command) curve = vertex1.curves()[0] cubit_command_or_exception(f"delete vertex {center_vertex.id()}") return curve
[docs] def create_surface_from_coordinates(coordinates): """Create a surface from an [N, 3] array of coordinates Each row of the array represents a coordinate in 3D space. Must have at least 3 rows or a RuntimeError is raised. Coordinates are connected in pairs to create curves. First and last coordinate connected for final curve. Curves must defind a closed perimeter to generate a surface. :param numpy.array coordinates: [N, 3] array of 3D coordinates where N > 2. :returns: Cubit surface object :rtype: cubit.surface """ coordinates = numpy.array(coordinates) if coordinates.shape[0] < 3: raise RuntimeError("Requires at least 3 coordinates to create a surface") curves = [] last = numpy.array([coordinates[-1]]) coordinates_shift = numpy.append(last, coordinates[0:-1], axis=0) for point1, point2 in zip(coordinates, coordinates_shift): curves.append(create_curve_from_coordinates(point1, point2)) return cubit.create_surface(curves)
[docs] def _surface_numbers(surfaces): """Return a list of surface IDs from the provided list of surface objects :param list surfaces: list of Cubit surface objects :returns: list of surface IDs :rtype: list of int """ return [surface.surfaces()[0].id() for surface in surfaces]
[docs] def _surface_centroids(surfaces): """Return a list of 3D surface centroids from the provided list of surface objects :param list surfaces: list of Cubit surface objects :returns: list of surface centroids :rtype: list of numpy.arrays """ surface_ids = _surface_numbers(surfaces) surface_centroids = [numpy.array(cubit.get_surface_centroid(id)) for id in surface_ids] return surface_centroids
[docs] def _surfaces_for_volumes(volumes): """Return a flat list of surface objects for a list of volumes :param list volumes: list of Cubit volume objects :returns: list of Cubit surface objects :rtype: list """ surfaces = [] for volume in volumes: surfaces.extend(volume.surfaces()) return surfaces
[docs] def _surfaces_by_vector(surfaces, principal_vector, center=numpy.zeros(3)): """Return a flat list of Cubit surface objects that meet the requirement of a positive dot product between a given vector and the vector between two points: a user provided center point and a surface object centroid. :param list surfaces: list of Cubit surface objects :param numpy.array principal_vector: Local principal axis vector defined in global coordinates :param numpy.array center: center location of the geometry :returns: numpy.array of Cubit surface objects :rtype: numpy.array """ surface_centroids = _surface_centroids(surfaces) direction_vectors = [numpy.subtract(centroid, center) for centroid in surface_centroids] vector_dot = numpy.array( ([numpy.dot(direction_vector, principal_vector) for direction_vector in direction_vectors]) ) # Account for numerical errors in significant digits vector_dot[numpy.isclose(vector_dot, 0.0)] = 0.0 return numpy.array(surfaces)[numpy.where(vector_dot > 0.0)]
[docs] def _create_volume_from_surfaces(surfaces, keep=True): """Create a volume from the provided surfaces. Surfaces must create a closed volume. :param list surfaces: List of Cubit surface objects :param bool keep: Keep the original surface objects/sheet bodies :returns: Cubit volume object :rtype: cubit.Volume """ volumes_before = cubit.get_entities("volume") surface_numbers = _surface_numbers(surfaces) surface_string = _utilities.character_delimited_list(surface_numbers) command = f"create volume surface {surface_string} heal" if keep: command = f"{command} keep" # TODO: Recover volume object directly when creation is possible with Cubit Python API cubit_command_or_exception(command) volumes_after = cubit.get_entities("volume") volume_id = list(set(volumes_after) - set(volumes_before)) volume_id = volume_id[0] return cubit.volume(volume_id)
[docs] def _rename_and_sweep( surface, part_name, center=numpy.array([0.0, 0.0, 0.0]), planar=parsers.geometry_defaults["planar"], revolution_angle=parsers.geometry_defaults["revolution_angle"], ): """Recover body or volume from body surface, sweep part if required, and rename body/volume by part name Hyphens are replaced by underscores to make the ACIS engine happy. :param cubit.Surface surface: Cubit surface object to rename and conditionally sweep :param str part_name: name of the part being created :param bool planar: switch to indicate that 2D model dimensionality is planar, not axisymmetric :param float revolution_angle: angle of solid revolution for ``3D`` geometries. Ignore when planar is True. :returns: Cubit volume object :rtype: cubit.Volume """ center = numpy.array(center) center_string = _utilities.character_delimited_list(center) revolution_axis = numpy.array([0.0, 1.0, 0.0]) revolution_string = _utilities.character_delimited_list(revolution_axis) body_number = surface.id() surface_number = _surface_numbers([surface])[0] part_name = part_name.replace("-", "_") if planar: return_object = surface.volumes()[0] elif numpy.isclose(revolution_angle, 0.0): return_object = surface.volumes()[0] else: cubit_command_or_exception( f"sweep surface {surface_number} axis {center_string} {revolution_string} " f"angle {revolution_angle} merge" ) return_object = surface.volumes()[0] volume_id = return_object.id() cubit_command_or_exception(f"regularize volume {volume_id}") return_object.set_entity_name(part_name) return return_object
[docs] def _get_volumes_from_name(names): """Return all volume objects with a prefix from the ``names`` list :param list names: Name(s) prefix to search for with ``cubit.get_all_ids_from_name`` :returns: list of Cubit volumes with name prefix :rtype: list of cubit.Volume objects """ if isinstance(names, str): names = [names] parts = [] for name in names: parts.extend([cubit.volume(number) for number in cubit.get_all_ids_from_name("volume", name)]) if len(parts) < 1: raise RuntimeError(f"Could not find any volumes with prefix '{name}'") return parts
[docs] def cylinder( inner_radius, outer_radius, height, output_file, part_name=parsers.cylinder_defaults["part_name"], revolution_angle=parsers.geometry_defaults["revolution_angle"], y_offset=parsers.cylinder_defaults["y_offset"], ): """Accept dimensions of a right circular cylinder and generate an axisymmetric revolved geometry Centroid of cylinder is located on the global coordinate origin by default. :param float inner_radius: Radius of the hollow center :param float outer_radius: Outer radius of the cylinder :param float height: Height of the cylinder :param str output_file: Cubit ``*.cub`` database to save the part(s) :param list part_name: name(s) of the part(s) being created :param float revolution_angle: angle of solid revolution for ``3D`` geometries :param float y_offset: vertical offset along the global Y-axis """ cubit.init(["cubit"]) part_name = _mixed_utilities.cubit_part_names(part_name) output_file = pathlib.Path(output_file).with_suffix(".cub") lines = vertices.cylinder_lines(inner_radius, outer_radius, height, y_offset=y_offset) surface = _draw_surface(lines, []) _rename_and_sweep(surface, part_name, revolution_angle=revolution_angle) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def sphere( inner_radius, outer_radius, output_file, input_file=parsers.sphere_defaults["input_file"], quadrant=parsers.sphere_defaults["quadrant"], revolution_angle=parsers.sphere_defaults["revolution_angle"], y_offset=parsers.sphere_defaults["y_offset"], part_name=parsers.sphere_defaults["part_name"], ): """ :param float inner_radius: inner radius (size of hollow) :param float outer_radius: outer radius (size of sphere) :param str output_file: output file name. Will be stripped of the extension and ``.cub`` will be used. :param str input_file: input file name. Will be stripped of the extension and ``.cub`` will be used. :param str quadrant: quadrant of XY plane for the sketch: upper (I), lower (IV), both :param float revolution_angle: angle of rotation 0.-360.0 degrees. Provide 0 for a 2D axisymmetric model. :param float y_offset: vertical offset along the global Y-axis :param str part_name: name of the part to be created in the Abaqus model """ cubit.init(["cubit"]) # Preserve the (X, Y) center implementation, but use the simpler y-offset interface center = (0.0, y_offset) part_name = _mixed_utilities.cubit_part_names(part_name) output_file = pathlib.Path(output_file).with_suffix(".cub") if input_file is not None: input_file = pathlib.Path(input_file).with_suffix(".cub") # Avoid modifying the contents or timestamp on the input file. # Required to get conditional re-builds with a build system such as GNU Make, CMake, or SCons with _utilities.NamedTemporaryFileCopy(input_file, suffix=".cub", dir=".") as copy_file: # TODO: look for a Cubit Python interface proper open/close/save command(s) cubit_command_or_exception(f"open '{copy_file.name}'") _sphere( inner_radius, outer_radius, quadrant=quadrant, revolution_angle=revolution_angle, center=center, part_name=part_name, ) cubit_command_or_exception(f"save as '{output_file}' overwrite") else: _sphere( inner_radius, outer_radius, quadrant=quadrant, revolution_angle=revolution_angle, center=center, part_name=part_name, ) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def _sphere( inner_radius, outer_radius, quadrant=parsers.sphere_defaults["quadrant"], revolution_angle=parsers.sphere_defaults["revolution_angle"], center=parsers.sphere_defaults["center"], part_name=parsers.sphere_defaults["part_name"], ): """ :param float inner_radius: inner radius (size of hollow) :param float outer_radius: outer radius (size of sphere) :param str quadrant: quadrant of XY plane for the sketch: upper (I), lower (IV), both :param float revolution_angle: angle of rotation 0.-360.0 degrees. Provide 0 for a 2D axisymmetric model. :param tuple center: tuple of floats (X, Y) location for the center of the sphere :param str part_name: name of the part to be created in the Abaqus model """ # TODO: consolidate pure Python 3 logic in a common module for both Gmsh and Cubit # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/boards arc_points = vertices.sphere(center, inner_radius, outer_radius, quadrant) inner_point1 = arc_points[0] inner_point2 = arc_points[1] outer_point1 = arc_points[2] outer_point2 = arc_points[3] center_3d = numpy.append(center, [0.0]) curves = [] if numpy.allclose(inner_point1, center) and numpy.allclose(inner_point2, center): inner_point1 = center inner_point2 = center else: curves.append(create_arc_from_coordinates(center_3d, inner_point1, inner_point2)) curves.append(create_arc_from_coordinates(center_3d, outer_point1, outer_point2)) curves.append(create_curve_from_coordinates(inner_point1, outer_point1)) curves.append(create_curve_from_coordinates(inner_point2, outer_point2)) surface = cubit.create_surface(curves) _rename_and_sweep(surface, part_name, revolution_angle=revolution_angle, center=center_3d)
[docs] def imprint_and_merge(names): """Imprint and merge all volume objects with a prefix from the ``names`` list :param list names: Name(s) prefix to search for with ``cubit.get_all_ids_from_name`` """ parts = _get_volumes_from_name(names) part_ids = [part.id() for part in parts] part_string = _utilities.character_delimited_list(part_ids) cubit_command_or_exception(f"imprint volume {part_string}") cubit_command_or_exception(f"merge volume {part_string}")
[docs] def webcut_local_coordinate_primary_planes(center, xvector, zvector, names): """Webcut all volumes with a prefix in the ``names`` list on the local coordinate system primary planes :param list center: center location of the geometry :param list xvector: Local x-axis vector defined in global coordinates :param list zvector: Local z-axis vector defined in global coordinates :param list names: Volume name prefix(es) to search for with ``cubit.get_all_ids_from_name`` :returns: list of Cubit volumes with name prefix(es) :rtype: list of cubit.Volume objects """ center = numpy.array(center) xvector = numpy.array(xvector) zvector = numpy.array(zvector) yvector = numpy.cross(zvector, xvector) # Create local coordinate system primary planes surface_coordinates = [ numpy.array([center, center + xvector, center + yvector]), numpy.array([center, center + yvector, center + zvector]), numpy.array([center, center + zvector, center + xvector]), ] primary_surfaces = [create_surface_from_coordinates(coordinates) for coordinates in surface_coordinates] primary_surface_numbers = _surface_numbers(primary_surfaces) primary_surface_string = _utilities.character_delimited_list(primary_surface_numbers) # Webcut with local coordinate system primary planes for number in primary_surface_numbers: parts = _get_volumes_from_name(names) part_ids = [part.id() for part in parts] part_string = _utilities.character_delimited_list(part_ids) cubit_command_or_exception(f"webcut volume {part_string} with plane from surface {number}") # Clean up primary surfaces cubit_command_or_exception(f"delete surface {primary_surface_string}") return _get_volumes_from_name(names)
[docs] def create_pyramid_volumes(center, xvector, zvector, size): """Return the six (6) four-sided pyramid volumes defined by a cube's center point and six outer faces :param list center: center location of the geometry :param list xvector: Local x-axis vector defined in global coordinates :param list zvector: Local z-axis vector defined in global coordinates :param float size: Half-length of the cube diagonals (length of the pyramid tip to corner) :returns: list of Cubit volumes :rtype: list of cubit.Volume objects """ center = numpy.array(center) xvector = numpy.array(xvector) zvector = numpy.array(zvector) yvector = numpy.cross(zvector, xvector) # Create 6 4-sided pyramidal bodies defining the partitioning intersections surface_coordinates = vertices.pyramid_surfaces(center, xvector, zvector, size) pyramid_surfaces = [create_surface_from_coordinates(coordinates) for coordinates in surface_coordinates] # Identify surfaces for individual pyramid volumes based on location relative to local coordinate system pyramid_volume_surfaces = [ _surfaces_by_vector(pyramid_surfaces, yvector, center), # +Y # fmt: skip # noqa: E241 _surfaces_by_vector(pyramid_surfaces, -yvector, center), # -Y # fmt: skip # noqa: E241 _surfaces_by_vector(pyramid_surfaces, xvector, center), # +X # fmt: skip # noqa: E241 _surfaces_by_vector(pyramid_surfaces, -xvector, center), # -X # fmt: skip # noqa: E241 _surfaces_by_vector(pyramid_surfaces, zvector, center), # +Z # fmt: skip # noqa: E241 _surfaces_by_vector(pyramid_surfaces, -zvector, center), # -Z # fmt: skip # noqa: E241 ] pyramid_volumes = [_create_volume_from_surfaces(surface_list) for surface_list in pyramid_volume_surfaces] # Remove pyramidal construction surfaces surface_numbers = _surface_numbers(pyramid_surfaces) surface_string = _utilities.character_delimited_list(surface_numbers) cubit_command_or_exception(f"delete surface {surface_string}") # TODO: ^^ Move pyramid volume creation to a dedicated function ^^ return pyramid_volumes
[docs] def create_pyramid_partitions(center, xvector, zvector, size, names): """Partition all volumes with a prefix in the ``names`` list with the size pyramids defined by a cube :param list center: center location of the geometry :param list xvector: Local x-axis vector defined in global coordinates :param list zvector: Local z-axis vector defined in global coordinates :param float size: Half-length of the cube diagonals (length of the pyramid tip to corner) :param list names: Volume name prefix(es) to search for with ``cubit.get_all_ids_from_name`` :returns: list of Cubit volumes :rtype: list of cubit.Volume objects """ # Create pyramid partitioning (intersecting) volumes pyramid_volumes = create_pyramid_volumes(center, xvector, zvector, size) pyramid_volume_numbers = [pyramid.id() for pyramid in pyramid_volumes] pyramid_volume_string = _utilities.character_delimited_list(pyramid_volume_numbers) # Create pyramidal intersections/partitions parts = _get_volumes_from_name(names) for volume in pyramid_volumes: volume_id = volume.id() for part in parts: cubit_command_or_exception(f"intersect volume {volume_id} with volume {part.id()} keep") for part in parts: cubit_command_or_exception(f"delete volume {part.id()}") # Clean up pyramid volumes cubit_command_or_exception(f"delete volume {pyramid_volume_string}") return _get_volumes_from_name(names)
[docs] def partition( input_file, output_file=parsers.partition_defaults["output_file"], center=parsers.partition_defaults["center"], xvector=parsers.partition_defaults["xvector"], zvector=parsers.partition_defaults["zvector"], part_name=parsers.partition_defaults["part_name"], big_number=parsers.partition_defaults["big_number"], ): """Partition Cubit files with pyramidal body intersections defined by a cube's center and vertices and with local coordinate planes. :param str input_file: Cubit ``*.cub`` file to open that already contains parts/volumes to be meshed :param str output_file: Cubit ``*.cub`` file to write :param list center: center location of the geometry :param list xvector: Local x-axis vector defined in global coordinates :param list zvector: Local z-axis vector defined in global coordinates :param list part_name: part/volume name prefixes :param float big_number: Number larger than the outer radius of the part to partition. """ cubit.init(["cubit"]) part_name = _mixed_utilities.cubit_part_names(part_name) if output_file is None: output_file = input_file input_file = pathlib.Path(input_file).with_suffix(".cub") output_file = pathlib.Path(output_file).with_suffix(".cub") with _utilities.NamedTemporaryFileCopy(input_file, suffix=".cub", dir=".") as copy_file: cubit_command_or_exception(f"open '{copy_file.name}'") _partition(center, xvector, zvector, part_name, big_number) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def _partition( center=parsers.partition_defaults["center"], xvector=parsers.partition_defaults["xvector"], zvector=parsers.partition_defaults["zvector"], part_name=parsers.partition_defaults["part_name"], big_number=parsers.partition_defaults["big_number"], ): """Partition Cubit files with pyramidal body intersections defined by a cube's center and vertices and with local coordinate planes. :param list center: center location of the geometry :param list xvector: Local x-axis vector defined in global coordinates :param list zvector: Local z-axis vector defined in global coordinates :param list part_name: part/volume name prefixes :param float big_number: Number larger than the outer radius of the part to partition. """ center = numpy.array(center) xvector = numpy.array(xvector) zvector = numpy.array(zvector) yvector = numpy.cross(zvector, xvector) # Create pyramidal intersections/partitions create_pyramid_partitions(center, xvector, zvector, big_number, part_name) # Webcut with local coordinate system primary planes webcut_local_coordinate_primary_planes(center, xvector, zvector, part_name) # Imprint and merge for current_part_name in part_name: imprint_and_merge([current_part_name])
[docs] def _set_from_mask(feature: str, name_mask: typing.Tuple[str, str]) -> None: """Create named features, with associated node and sidesets, by feature ID :param feature: Cubit feature name :param name_mask: Feature set tuples (name, ID string) """ feature = feature.lower() for name, mask in name_mask: cubit_command_or_exception(f'{feature} {mask} name "{name}"') nodeset_id = cubit.get_next_nodeset_id() cubit_command_or_exception(f"nodeset {nodeset_id} ADD {feature} {mask}") cubit_command_or_exception(f'nodeset {nodeset_id} name "{name}"') if feature not in ("vertex", "node"): sideset_id = cubit.get_next_sideset_id() cubit_command_or_exception(f"sideset {sideset_id} ADD {feature} {mask}") cubit_command_or_exception(f'sideset {sideset_id} name "{name}"')
[docs] def _feature_seeds(feature: str, name_number: typing.Tuple[str, str]) -> None: """Create mesh seeds on features by name If the number is an integer, seed by interval. If the number is a float, seed by size :param feature: Cubit feature name :param name_number: Feature seed tuples (name, number) """ names, numbers = zip(*name_number) numbers = [float(number) for number in numbers] positive_numbers = [number > 0.0 for number in numbers] if not all(positive_numbers): raise ValueError("Feature seeds must be positive numbers") for name, number in zip(names, numbers): feature_ids = _utilities.character_delimited_list(cubit.get_all_ids_from_name(feature, name)) if number.is_integer(): cubit_command_or_exception(f"{feature} {feature_ids} interval {int(number)}") else: cubit_command_or_exception(f"{feature} {feature_ids} size {number}")
[docs] def _sets( face_sets: typing.Optional[typing.List] = parsers.sets_defaults["face_sets"], edge_sets: typing.Optional[typing.List] = parsers.sets_defaults["edge_sets"], vertex_sets: typing.Optional[typing.List] = parsers.sets_defaults["vertex_sets"], ) -> None: """Create named features, with associated node and sidesets, by feature ID :param face_sets: Face set tuples (name, mask) :param edge_sets: Edge set tuples (name, mask) :param vertex_sets: Vertex set tuples (name, mask) """ if face_sets is not None: _set_from_mask("surface", face_sets) if edge_sets is not None: _set_from_mask("curve", edge_sets) if vertex_sets is not None: _set_from_mask("vertex", vertex_sets)
[docs] def sets( input_file: str, output_file: typing.Optional[str] = parsers.sets_defaults["output_file"], part_name: typing.Optional[str] = parsers.sets_defaults["part_name"], face_sets: typing.Optional[typing.List] = parsers.sets_defaults["face_sets"], edge_sets: typing.Optional[typing.List] = parsers.sets_defaults["edge_sets"], vertex_sets: typing.Optional[typing.List] = parsers.sets_defaults["vertex_sets"], ) -> None: """Create Cubit sidesets and nodesets from feature numbers :param input_file: Cubit ``*.cub`` file to open that already contains parts/volumes to be meshed :param output_file: Cubit ``*.cub`` file to write :param part_name: part/volume name prefix :param face_sets: Face set tuples (name, mask) :param edge_sets: Edge set tuples (name, mask) :param vertex_sets: Vertex set tuples (name, mask) """ cubit.init(["cubit"]) part_name = _mixed_utilities.cubit_part_names(part_name) if not any([face_sets, edge_sets, vertex_sets]): raise RuntimeError("Must specify at least one of: face_sets, edge_sets, vertex_sets") if output_file is None: output_file = input_file input_file = pathlib.Path(input_file).with_suffix(".cub") output_file = pathlib.Path(output_file).with_suffix(".cub") with _utilities.NamedTemporaryFileCopy(input_file, suffix=".cub", dir=".") as copy_file: cubit_command_or_exception(f"open '{copy_file.name}'") _sets(face_sets, edge_sets, vertex_sets) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def mesh( input_file: str, element_type: str, output_file: typing.Optional[str] = parsers.mesh_defaults["output_file"], part_name: typing.Optional[str] = parsers.mesh_defaults["part_name"], global_seed: typing.Optional[float] = parsers.mesh_defaults["global_seed"], edge_seeds: typing.Optional[typing.List] = parsers.mesh_defaults["edge_seeds"], ) -> None: """Mesh Cubit volumes and sheet bodies by part/volume name :param input_file: Cubit ``*.cub`` file to open that already contains parts/volumes to be meshed :param element_type: Cubit scheme "trimesh" or "tetmesh". Else ignored. :param output_file: Cubit ``*.cub`` file to write :param part_name: part/volume name prefix :param global_seed: The global mesh seed size :param edge_seeds: Edge seed tuples (name, number) """ cubit.init(["cubit"]) part_name = _mixed_utilities.cubit_part_names(part_name) if output_file is None: output_file = input_file input_file = pathlib.Path(input_file).with_suffix(".cub") output_file = pathlib.Path(output_file).with_suffix(".cub") with _utilities.NamedTemporaryFileCopy(input_file, suffix=".cub", dir=".") as copy_file: cubit_command_or_exception(f"open '{copy_file.name}'") _mesh(element_type, part_name, global_seed, edge_seeds) cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def _mesh_sheet_body(volume, global_seed, element_type=None): """Mesh a volume that is a sheet body Assumes ``cubit.is_sheet_body(volume.id())`` is ``True``. :param cubit.Volume volume: Cubit volume to mesh as a sheet body :param float global_seed: Seed size, e.g. ``cubit.cmd(surface {} size {global_seed}`` :param str element_type: Cubit meshing scheme. Accepts 'trimesh' or is ignored. """ # TODO: Process multiple sheet bodies with a single Cubit command set # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/80 surface_objects = volume.surfaces() surfaces = [surface.id() for surface in surface_objects] surface_string = _utilities.character_delimited_list(surfaces) if element_type == "trimesh": cubit_command_or_exception(f"surface {surface_string} scheme {element_type}") cubit_command_or_exception(f"surface {surface_string} size {global_seed}") for surface in surface_objects: surface.mesh()
[docs] def _mesh_volume(volume, global_seed, element_type=None): """Mesh a volume :param cubit.Volume volume: Cubit volume to mesh :param float global_seed: Seed size, e.g. ``cubit.cmd(volume {} size {global_seed}`` :param str element_type: Cubit meshing scheme. Accepts 'tetmesh' or is ignored. """ # TODO: Process multiple volumes with a single Cubit command set # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/80 volume_id = volume.id() if element_type == "tetmesh": cubit_command_or_exception(f"volume {volume_id} scheme {element_type}") cubit_command_or_exception(f"volume {volume_id} size {global_seed}") volume.mesh()
[docs] def _mesh_multiple_volumes(volumes, global_seed, element_type=None): """Mesh ``cubit.Volume`` objects as volumes or sheet bodies :param list volumes: list of Cubit volume objects to mesh """ # TODO: Process all sheet bodies and all volumes with a single Cubit command set # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/80 for volume in volumes: volume_id = volume.id() if cubit.is_sheet_body(volume_id): _mesh_sheet_body(volume, global_seed, element_type=element_type) else: _mesh_volume(volume, global_seed, element_type=element_type)
[docs] def _mesh(element_type, part_name, global_seed, edge_seeds): """Mesh Cubit volumes and sheet bodies by part/volume name :param str element_type: Cubit scheme "trimesh" or "tetmesh". Else ignored. :param str part_name: part/volume name prefix :param float global_seed: The global mesh seed size :param edge_seeds: Edge seed tuples (name, number) """ parts = _get_volumes_from_name(part_name) element_type = element_type.lower() # TODO: Cubit can support more than just edge seeds # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/174 if edge_seeds is not None: _feature_seeds("curve", edge_seeds) _mesh_multiple_volumes(parts, global_seed, element_type=element_type)
[docs] def merge(input_file, output_file): """Merge Cubit ``*.cub`` files with forced unique block IDs and save to output file :param list input_file: List of Cubit ``*.cub`` file(s) to merge :param str output_file: Cubit ``*.cub`` file to write """ cubit.init(["cubit"]) input_file = [pathlib.Path(path).with_suffix(".cub") for path in input_file] output_file = pathlib.Path(output_file).with_suffix(".cub") for path in input_file: cubit_command_or_exception(f"import cubit '{path}' unique_genesis_ids") cubit_command_or_exception(f"save as '{output_file}' overwrite")
[docs] def export( input_file, part_name=parsers.export_defaults["part_name"], element_type=parsers.export_defaults["element_type"], destination=parsers.export_defaults["destination"], output_type=parsers.export_defaults["output_type"], ): """Open a Cubit ``*.cub`` file and export ``part_name`` prefixed volumes as ``part_name``.inp :param str input_file: Cubit ``*.cub`` file to open that already contains meshed parts/volumes :param str part_name: list of part/volume name prefix to export :param list element_type: list of element types, one per part name or one global replacement for every part name :param str destination: write output orphan mesh files to this output directory """ cubit.init(["cubit"]) part_name = _mixed_utilities.cubit_part_names(part_name) element_type = _mixed_utilities.validate_element_type(length_part_name=len(part_name), element_type=element_type) input_file = pathlib.Path(input_file).with_suffix(".cub") destination = pathlib.Path(destination) cubit_command_or_exception(f"open '{input_file}'") if output_type.lower() == "abaqus": _export_abaqus_list(part_name, element_type, destination) elif output_type.lower().startswith("genesis"): output_file = destination / input_file.with_suffix(".g").name _export_genesis(output_file, part_name, element_type, output_type) else: raise RuntimeError(f"Uknown output type request '{output_type}'")
[docs] def _create_new_block(volumes): """Create a new block for all volumes in list Sheet bodies are added to block as surfaces. Volumes are added as volumes. :param list volumes: list of Cubit volume objects :returns: new block ID :rtype: int """ new_block_id = cubit.get_next_block_id() volume_ids = [volume.id() for volume in volumes] volume_string = _utilities.character_delimited_list(volume_ids) if any([cubit.is_sheet_body(volume_id) for volume_id in volume_ids]): surfaces = _surface_numbers(_surfaces_for_volumes(volumes)) surface_string = _utilities.character_delimited_list(surfaces) cubit_command_or_exception(f"block {new_block_id} add surface {surface_string}") else: cubit_command_or_exception(f"block {new_block_id} add volume {volume_string}") return new_block_id
[docs] def _create_volume_name_block(name): """Create a new block with all volumes prefixed by name :param str name: Name for new block and prefix for volume search :returns: New block ID :rtype: int """ volumes = _get_volumes_from_name(name) new_block_id = _create_new_block(volumes) cubit_command_or_exception(f"block {new_block_id} name '{name}'") return new_block_id
[docs] def _set_genesis_output_type(output_type): """Set Cubit exodus/genesis output type :param str output_type: String identifying genesis output type: genesis (large format), genesis-normal, genesis-hdf5 """ if output_type.lower() == "genesis": cubit_command_or_exception(f"set large exodus file on") elif output_type.lower() == "genesis-normal": cubit_command_or_exception(f"set large exodus file off") elif output_type.lower() == "genesis-hdf5": cubit_command_or_exception(f"set exodus netcdf4 on") else: raise RuntimeError("Unknown genesis output type '{output_type}'")
[docs] def _export_genesis(output_file, part_name, element_type, output_type="genesis"): """Export all volumes with part name prefix to the output file Always creates new blocks named after the part/volume prefix. :param pathlib.Path output_file: Genesis file to write :param list part_name: list of part/volume names to create as blocks from all volumes with a matching prefix :param list element_type: list of element type strings :param str output_type: String identifying genesis output type: genesis (large format), genesis-normal, genesis-hdf5 """ block_ids = [] for name, element in zip(part_name, element_type): block_ids.append(_create_volume_name_block(name)) if element_type is not None: cubit_command_or_exception(f"block {block_ids[-1]} element type {element}") _set_genesis_output_type(output_type) block_string = _utilities.character_delimited_list(block_ids) cubit_command_or_exception(f"export mesh '{output_file}' block {block_string} overwrite")
[docs] def _export_abaqus_list(part_name, element_type, destination): """Export one Abaqus orphan mesh per part in the destination directory :param list part_name: list of part/volume names to create as blocks from all volumes with a matching prefix :param list element_type: List of element type strings :param pathlib.Path destination: Parent directory for orphan mesh files """ for name, element in zip(part_name, element_type): output_file = destination / name output_file = output_file.with_suffix(".inp") _export_abaqus(output_file, name) if element is not None: _mixed_utilities.substitute_element_type(output_file, element)
[docs] def _export_abaqus(output_file, part_name): """Create a block named after the part, add all volumes/surfaace with name prefix, export an Abaqus orphan mesh file :param pathlib.Path output_file: Abaqus file to write :param str part_name: part/volume name to create as blocks from all volumes with a matching prefix """ new_block_id = _create_volume_name_block(part_name) cubit_command_or_exception(f"export abaqus '{output_file}' block {new_block_id} partial overwrite")
[docs] def image( input_file, output_file, cubit_command, x_angle=parsers.image_defaults["x_angle"], y_angle=parsers.image_defaults["y_angle"], z_angle=parsers.image_defaults["z_angle"], image_size=parsers.image_defaults["image_size"], ): """Open a Cubit ``*.cub`` file and save an image Uses the Cubit APREPRO `hardcopy`_ command, which accepts jpg, gif, bmp, pnm, tiff, and eps file extensions. This command only works in batch mode from Cubit APREPRO journal files, so an ``input_file``.jou is created for execution. :param str input_file: Cubit ``*.cub`` file to open that already contains parts/volumes to be meshed :param str output_file: Screenshot file to write :param float x_angle: Rotation about 'world' X-axis in degrees :param float y_angle: Rotation about 'world' Y-axis in degrees :param float z_angle: Rotation about 'world' Z-axis in degrees :param tuple image_size: Image size in pixels (width, height) """ input_file = pathlib.Path(input_file).with_suffix(".cub") output_file = pathlib.Path(output_file) output_type = output_file.suffix.strip(".") journal_path = output_file.with_suffix(".jou") with open(journal_path, "w") as journal_file: journal_file.write(f"open '{input_file}'\n") journal_file.write(f"graphics windowsize {image_size[0]} {image_size[1]}\n") journal_file.write(f"rotate {x_angle} about world x\n") journal_file.write(f"rotate {y_angle} about world y\n") journal_file.write(f"rotate {z_angle} about world z\n") journal_file.write(f"hardcopy '{output_file}' {output_type}\n") command = f"{cubit_command} -batch {journal_path}" _utilities.run_command(command)