Source code for turbo_turtle._gmsh_python

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

import pathlib
import typing

import numpy

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

gmsh = _utilities.import_gmsh()


[docs] def geometry( input_file: typing.Sequence[str | pathlib.Path], output_file: str | pathlib.Path, planar: bool = parsers.geometry_defaults["planar"], # type: ignore[assignment] model_name: str = parsers.geometry_defaults["model_name"], # type: ignore[assignment] part_name: str = parsers.geometry_defaults["part_name"], # type: ignore[assignment] unit_conversion: float = parsers.geometry_defaults["unit_conversion"], # type: ignore[assignment] euclidean_distance: float = parsers.geometry_defaults["euclidean_distance"], # type: ignore[assignment] delimiter: str = parsers.geometry_defaults["delimiter"], # type: ignore[assignment] header_lines: int = parsers.geometry_defaults["header_lines"], # type: ignore[assignment] revolution_angle: float = parsers.geometry_defaults["revolution_angle"], # type: ignore[assignment] y_offset: float = parsers.geometry_defaults["y_offset"], # type: ignore[assignment] rtol: float = parsers.geometry_defaults["rtol"], # type: ignore[assignment] atol: float = parsers.geometry_defaults["atol"], # type: ignore[assignment] ) -> 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 input_file: input text file(s) with coordinates to draw :param output_file: Gmsh ``*.step`` database to save the part(s) :param planar: switch to indicate that 2D model dimensionality is planar, not axisymmetric :param model_name: name of the Gmsh model in which to create the part :param part_name: name(s) of the part(s) being created :param unit_conversion: multiplication factor applies to all coordinates :param 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 delimiter: character to use as a delimiter when reading the input file :param header_lines: number of lines in the header to skip when reading the input file :param revolution_angle: angle of solid revolution for ``3D`` geometries. Ignore when planar is True. :param y_offset: vertical offset along the global Y-axis. Offset should be provided in units *after* the unit conversion. :param rtol: relative tolerance for vertical/horizontal line checks :param 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, strict=True): 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, strict=True): _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: list[numpy.ndarray]) -> int: """Given ordered lists of line/spline coordinates, create a Gmsh 2D surface object. :param 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: tuple[float, float, float], point2: tuple[float, float, float]) -> int: """Create a curve from 2 three-dimensional coordinates. :param point1: First set of coordinates (x1, y1, z1) :param 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: typing.Sequence[tuple[float, float, float]] | numpy.ndarray) -> int: """Create a spline from a sequence of coordinates. :param 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 = [gmsh.model.occ.addPoint(*tuple(point)) for point in coordinates] return gmsh.model.occ.addBSpline(points)
[docs] def _rename_and_sweep( surface: int, part_name: str, center: tuple[float, float, float] | numpy.ndarray = (0.0, 0.0, 0.0), planar: bool = parsers.geometry_defaults["planar"], # type: ignore[assignment] revolution_angle: float = parsers.geometry_defaults["revolution_angle"], # type: ignore[assignment] ) -> 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 surface: Gmsh surface tag to rename and conditionally sweep :param part_name: name(s) of the part(s) being created :param center: coordinate location for the center of axisymmetric sweep :param planar: switch to indicate that 2D model dimensionality is planar, not axisymmetric :param 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: # Using naming convention of the external library, Gmsh dimTags = gmsh.model.occ.revolve( # noqa: N806 [(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: float, outer_radius: float, height: float, output_file: str | pathlib.Path, model_name: str = parsers.geometry_defaults["model_name"], # type: ignore[assignment] part_name: str = parsers.cylinder_defaults["part_name"], # type: ignore[assignment] revolution_angle: float = parsers.geometry_defaults["revolution_angle"], # type: ignore[assignment] y_offset: float = parsers.cylinder_defaults["y_offset"], # type: ignore[assignment] ) -> 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 inner_radius: Radius of the hollow center :param outer_radius: Outer radius of the cylinder :param height: Height of the cylinder :param output_file: Gmsh ``*.step`` file to save the part(s) :param model_name: name of the Gmsh model in which to create the part :param part_name: name(s) of the part(s) being created :param revolution_angle: angle of solid revolution for ``3D`` geometries :param 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: float, outer_radius: float, output_file: str | pathlib.Path, input_file: str | pathlib.Path | None = parsers.sphere_defaults["input_file"], # type: ignore[assignment] quadrant: typing.Literal["upper", "lower", "both"] = parsers.sphere_defaults["quadrant"], # type: ignore[assignment] revolution_angle: float = parsers.sphere_defaults["revolution_angle"], # type: ignore[assignment] y_offset: float = parsers.sphere_defaults["y_offset"], # type: ignore[assignment] model_name: str = parsers.sphere_defaults["model_name"], # type: ignore[assignment] part_name: str = parsers.sphere_defaults["part_name"], # type: ignore[assignment] ) -> None: """Create a sphere geometry with file I/O handling. :param inner_radius: inner radius (size of hollow) :param outer_radius: outer radius (size of sphere) :param output_file: output file name. Will be stripped of the extension and ``.step`` will be used. :param input_file: input file name. Will be stripped of the extension and ``.step`` will be used. :param quadrant: quadrant of XY plane for the sketch: upper (I), lower (IV), both :param revolution_angle: angle of rotation 0.-360.0 degrees. Provide 0 for a 2D axisymmetric model. :param y_offset: vertical offset along the global Y-axis :param model_name: name of the Gmsh model in which to create the part :param 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: float, outer_radius: float, quadrant: str = parsers.sphere_defaults["quadrant"], # type: ignore[assignment] revolution_angle: float = parsers.sphere_defaults["revolution_angle"], # type: ignore[assignment] center: tuple[float, float] = parsers.sphere_defaults["center"], # type: ignore[assignment] part_name: str = parsers.sphere_defaults["part_name"], # type: ignore[assignment] ) -> None: """Create a sphere geometry without file I/O handling. :param inner_radius: inner radius (size of hollow) :param outer_radius: outer radius (size of sphere) :param quadrant: quadrant of XY plane for the sketch: upper (I), lower (IV), both :param revolution_angle: angle of rotation 0.-360.0 degrees. Provide 0 for a 2D axisymmetric model. :param center: tuple of floats (X, Y) location for the center of the sphere :param 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: tuple[float, float, float] | numpy.ndarray, point1: tuple[float, float, float] | numpy.ndarray, point2: tuple[float, float, float] | numpy.ndarray, ) -> int: """Create a circle arc Gmsh object from center and points on the curve. :param center: tuple of floats (X, Y, Z) location for the center of the circle arc :param point1: tuple of floats (X, Y, Z) location for the first point on the arc :param 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)
# TODO: Remove ``noqa: ARG001`` when this function is implemented. # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/212 def partition(*args, **kwargs) -> typing.NoReturn: # noqa: ARG001 raise RuntimeError("partition subcommand is not yet implemented") # TODO: Remove ``noqa: ARG001`` when this function is implemented. def sets(*args, **kwargs) -> typing.NoReturn: # noqa: ARG001 raise RuntimeError("sets subcommand is not yet implemented") # Argument(s) retained for compatibility with ``_cubit_python.mesh`` API
[docs] def mesh( input_file: str | pathlib.Path, element_type: str, # noqa: ARG001 output_file: str | pathlib.Path | None = parsers.mesh_defaults["output_file"], # type: ignore[assignment] model_name: str | None = parsers.mesh_defaults["model_name"], # type: ignore[assignment] # noqa: ARG001 part_name: str | None = parsers.mesh_defaults["part_name"], # type: ignore[assignment] # noqa: ARG001 global_seed: float = parsers.mesh_defaults["global_seed"], # type: ignore[assignment] edge_seeds: typing.Sequence[tuple[str, str | int | float]] | None = parsers.mesh_defaults["edge_seeds"], # type: ignore[assignment] # noqa: ARG001 ) -> 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()
# TODO: Remove ``noqa: ARG001`` when this function is implemented. # https://re-git.lanl.gov/aea/python-projects/turbo-turtle/-/issues/216 def merge(*args, **kwargs) -> typing.NoReturn: # noqa: ARG001 raise RuntimeError("merge subcommand is not yet implemented") # TODO: Remove ``noqa: ARG001`` when this function is implemented. def export(*args, **kwargs) -> typing.NoReturn: # noqa: ARG001 raise RuntimeError("export subcommand is not yet implemented")
[docs] def image( input_file: str | pathlib.Path, output_file: str | pathlib.Path, x_angle: float = parsers.image_defaults["x_angle"], # type: ignore[assignment] y_angle: float = parsers.image_defaults["y_angle"], # type: ignore[assignment] z_angle: float = parsers.image_defaults["z_angle"], # type: ignore[assignment] # ARG001: Argument retained for compatibility with ``_cubit_python.image`` API image_size: tuple[int, int] = parsers.image_defaults["image_size"], # type: ignore[assignment] # noqa: ARG001 ) -> 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()