Source code for turbo_turtle._gmsh_python

"""Python 3 module that imports python-gmsh"""

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


gmsh = _utilities.import_gmsh()


[docs] def geometry( input_file, output_file, planar=parsers.geometry_defaults["planar"], model_name=parsers.geometry_defaults["model_name"], 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"], ) -> None: """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 surfaces or volumes in the same Gmsh ``*.step`` 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: Gmsh ``*.step`` database to save the part(s) :param bool planar: switch to indicate that 2D model dimensionality is planar, not axisymmetric :param str model_name: name of the Gmsh model in which to create the part :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}.step`` """ # Universally required setup gmsh.initialize() gmsh.logger.start() # Input/Output setup # TODO: allow other output formats supported by Gmsh output_file = pathlib.Path(output_file).with_suffix(".step") # Model setup gmsh.model.add(model_name) part_name = _mixed_utilities.validate_part_name(input_file, part_name) part_name = _mixed_utilities.cubit_part_names(part_name) # Create part(s) 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_and_splines = vertices.ordered_lines_and_splines(coordinates, euclidean_distance, rtol=rtol, atol=atol) surfaces.append(_draw_surface(lines_and_splines)) # Conditionally create the 3D revolved shape for surface, new_part in zip(surfaces, part_name): _rename_and_sweep(surface, new_part, planar=planar, revolution_angle=revolution_angle) # Output and cleanup # FIXME: Write physical groups to geometry output files # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/221 gmsh.write(str(output_file)) gmsh.logger.stop() gmsh.finalize()
[docs] def _draw_surface(lines_and_splines) -> int: """Given ordered lists of line/spline coordinates, create a Gmsh 2D surface object :param list lines_and_splines: list of [N, 2] shaped arrays of (x, y) coordinates defining a line (N=2) or spline (N>2) :returns: Gmsh 2D entity tag """ curves = [] for coordinates in lines_and_splines: if len(coordinates) == 2: point1 = tuple(coordinates[0]) + (0.0,) point2 = tuple(coordinates[1]) + (0.0,) curves.append(_create_line_from_coordinates(point1, point2)) else: zero_column = numpy.zeros([len(coordinates), 1]) spline_3d = numpy.append(coordinates, zero_column, axis=1) curves.append(_create_spline_from_coordinates(spline_3d)) curve_loop = gmsh.model.occ.addCurveLoop(curves) return gmsh.model.occ.addPlaneSurface([curve_loop])
[docs] def _create_line_from_coordinates(point1, point2) -> int: """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: Gmsh 1D entity tag """ point1_tag = gmsh.model.occ.addPoint(*point1) point2_tag = gmsh.model.occ.addPoint(*point2) return gmsh.model.occ.addLine(point1_tag, point2_tag)
[docs] def _create_spline_from_coordinates(coordinates) -> int: """Create a spline from a list of coordinates :param numpy.array coordinates: [N, 3] array of coordinates (x, y, z) :returns: Gmsh 1D entity tag """ 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(gmsh.model.occ.addPoint(*tuple(point))) return gmsh.model.occ.addBSpline(points)
[docs] def _rename_and_sweep( surface: int, part_name: str, center=numpy.array([0.0, 0.0, 0.0]), planar=parsers.geometry_defaults["planar"], revolution_angle=parsers.geometry_defaults["revolution_angle"], ) -> typing.Tuple[int, int]: """Recover surface, sweep part if required, and rename surface/volume by part name Hyphens are replaced by underscores to make the ACIS engine happy. :param int surface: Gmsh surface tag to rename and conditionally sweep :param str part_name: name(s) of the part(s) 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: Gmsh dimTag (dimension, tag) """ center = numpy.array(center) revolution_axis = numpy.array([0.0, 1.0, 0.0]) if planar: dim_tag = (2, surface) elif numpy.isclose(revolution_angle, 0.0): dim_tag = (2, surface) else: dimTags = gmsh.model.occ.revolve( [(2, surface)], *center, *revolution_axis, numpy.radians(revolution_angle), ) dim_tag = dimTags[0] part_dimension = dim_tag[0] part_tag = dim_tag[0] part_name = _mixed_utilities.cubit_part_names(part_name) gmsh.model.occ.synchronize() part_tag = gmsh.model.addPhysicalGroup(part_dimension, [part_tag], name=part_name) return dim_tag
[docs] def cylinder( inner_radius, outer_radius, height, output_file, model_name=parsers.geometry_defaults["model_name"], part_name=parsers.cylinder_defaults["part_name"], revolution_angle=parsers.geometry_defaults["revolution_angle"], y_offset=parsers.cylinder_defaults["y_offset"], ) -> None: """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: Gmsh ``*.step`` file to save the part(s) :param str model_name: name of the Gmsh model in which to create the part :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 """ # Universally required setup gmsh.initialize() gmsh.logger.start() # Input/Output setup # TODO: allow other output formats supported by Gmsh output_file = pathlib.Path(output_file).with_suffix(".step") gmsh.model.add(model_name) # Create the 2D axisymmetric shape lines = vertices.cylinder_lines(inner_radius, outer_radius, height, y_offset=y_offset) surface_tag = _draw_surface(lines) # Conditionally create the 3D revolved shape _rename_and_sweep(surface_tag, part_name, revolution_angle=revolution_angle) # Output and cleanup # FIXME: Write physical groups to geometry output files # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/221 gmsh.write(str(output_file)) gmsh.logger.stop() gmsh.finalize()
[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"], model_name=parsers.sphere_defaults["model_name"], part_name=parsers.sphere_defaults["part_name"], ) -> None: """ :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 ``.step`` will be used. :param str input_file: input file name. Will be stripped of the extension and ``.step`` 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 model_name: name of the Gmsh model in which to create the part :param str part_name: name of the part to be created in the Abaqus model """ # Universally required setup gmsh.initialize() gmsh.logger.start() # Input/Output setup # TODO: allow other output formats supported by Gmsh output_file = pathlib.Path(output_file).with_suffix(".step") # Preserve the (X, Y) center implementation, but use the simpler y-offset interface center = (0.0, y_offset) if input_file is not None: # TODO: allow other input formats supported by Gmsh input_file = pathlib.Path(input_file).with_suffix(".step") # 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=".step", dir=".") as copy_file: gmsh.open(copy_file.name) _sphere( inner_radius, outer_radius, quadrant=quadrant, revolution_angle=revolution_angle, center=center, part_name=part_name, ) # FIXME: Write physical groups to geometry output files # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/221 gmsh.write(str(output_file)) else: gmsh.model.add(model_name) _sphere( inner_radius, outer_radius, quadrant=quadrant, revolution_angle=revolution_angle, center=center, part_name=part_name, ) # FIXME: Write physical groups to geometry output files # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/221 gmsh.write(str(output_file)) # Output and cleanup gmsh.logger.stop() gmsh.finalize()
[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"], ) -> None: """ :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) center_3d = numpy.append(center, [0.0]) zero_column = numpy.zeros([len(arc_points), 1]) arc_points_3d = numpy.append(arc_points, zero_column, axis=1) inner_point1 = arc_points_3d[0] inner_point2 = arc_points_3d[1] outer_point1 = arc_points_3d[2] outer_point2 = arc_points_3d[3] curves = [] if numpy.allclose(inner_point1, center_3d) and numpy.allclose(inner_point2, center_3d): inner_point1 = center_3d inner_point2 = center_3d else: curves.append(_create_arc_from_coordinates(center_3d, inner_point1, inner_point2)) curves.append(_create_line_from_coordinates(inner_point2, outer_point2)) curves.append(_create_arc_from_coordinates(center_3d, outer_point2, outer_point1)) curves.append(_create_line_from_coordinates(outer_point1, inner_point1)) curve_loop = gmsh.model.occ.addCurveLoop(curves) surface = gmsh.model.occ.addPlaneSurface([curve_loop]) _rename_and_sweep(surface, part_name, revolution_angle=revolution_angle, center=center_3d)
[docs] def _create_arc_from_coordinates(center, point1, point2) -> int: """Create a circle arc Gmsh 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: Gmsh curve tag """ center_tag = gmsh.model.occ.addPoint(*center) point1_tag = gmsh.model.occ.addPoint(*point1) point2_tag = gmsh.model.occ.addPoint(*point2) return gmsh.model.occ.addCircleArc(point1_tag, center_tag, point2_tag, center=True)
def partition(*args, **kwargs): raise RuntimeError("partition subcommand is not yet implemented") def sets(*args, **kwargs): raise RuntimeError("sets subcommand is not yet implemented")
[docs] def mesh( input_file: str, element_type: str, output_file: typing.Optional[str] = parsers.mesh_defaults["output_file"], model_name: typing.Optional[str] = parsers.mesh_defaults["model_name"], 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 Gmsh physical entities by part name :param input_file: Gmsh ``*.step`` file to open that already contains physical entities to be meshed :param element_type: Gmsh scheme. :param output_file: Gmsh mesh file to write :param model_name: name of the Gmsh model in which to create the part :param part_name: physical entity name prefix :param global_seed: The global mesh seed size :param edge_seeds: Edge seed tuples (name, number) """ # Universally required setup gmsh.initialize() gmsh.logger.start() # Input/Output setup # TODO: allow other output formats supported by Gmsh input_file = pathlib.Path(input_file).with_suffix(".step") if output_file is None: output_file = input_file.with_suffix(".msh") output_file = pathlib.Path(output_file) with _utilities.NamedTemporaryFileCopy(input_file, suffix=input_file.suffix, dir=".") as copy_file: gmsh.open(copy_file.name) # TODO: Move to dedicated meshing function # TODO: Do physical group names apply to all dimensional entities associated with original name? Can we jump # straight to points with matching physical/entity names? # FIXME: The physical groups are not getting saved. Stop global application of seed without regard to # part/entity name. # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/222 points = gmsh.model.getEntities(dim=0) gmsh.model.mesh.setSize(points, global_seed) gmsh.model.mesh.generate(3) gmsh.write(str(output_file)) gmsh.option.setNumber("Mesh.SaveGroupsOfElements", 1) gmsh.option.setNumber("Mesh.SaveGroupsOfNodes", 1) # Output and cleanup gmsh.logger.stop() gmsh.finalize()
def merge(*args, **kwargs): raise RuntimeError("merge subcommand is not yet implemented") def export(*args, **kwargs): raise RuntimeError("export subcommand is not yet implemented")
[docs] def image( input_file, output_file, 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"], ) -> None: """Open a Gmsh geometry or mesh file and save an image Uses the Gmsh ``write`` command, which accepts gif, jpg, tex, pdf, png, pgf, ps, ppm, svg, tikz, and yuv file extensions. :param str input_file: Gmsh input file to open :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) """ # Universally required setup gmsh.initialize() gmsh.logger.start() # Input/Output setup input_file = pathlib.Path(input_file) output_file = pathlib.Path(output_file) gmsh.open(str(input_file)) gmsh.fltk.initialize() gmsh.option.setNumber("General.Trackball", 0) gmsh.option.setNumber("General.RotationX", x_angle) gmsh.option.setNumber("General.RotationY", y_angle) gmsh.option.setNumber("General.RotationZ", z_angle) # Output and cleanup gmsh.write(str(output_file)) gmsh.logger.stop() gmsh.finalize()