Geometry Tutorial

Introduction

Geometry is not plasma physics, but it isn’t trivial either. Chances are most of your day-to-day interaction with bluemira will revolve around geometry in some form or another. Puns intended.

There a few basic concepts you need to familiarise yourself with:

  • Basic objects: [BluemiraWire, BluemiraFace, BluemiraShell, BluemiraSolid]

  • Basic properties

  • Matryoshka structure

  • Geometry creation

  • Geometry modification

  • Geometry operations

Imports

Let’s start out by importing all the basic objects, and some typical tools

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from bluemira.base.file import get_bluemira_path

# Some display functionality
from bluemira.display import plotter, show_cad
from bluemira.display.displayer import DisplayCADOptions

# Basic objects
from bluemira.geometry.coordinates import Coordinates
from bluemira.geometry.face import BluemiraFace
from bluemira.geometry.shell import BluemiraShell
from bluemira.geometry.solid import BluemiraSolid

# Some useful tools
from bluemira.geometry.tools import (
    boolean_cut,
    boolean_fuse,
    extrude_shape,
    interpolate_bspline,
    make_circle,
    make_polygon,
    revolve_shape,
    save_cad,
    sweep_shape,
)
from bluemira.geometry.wire import BluemiraWire

Geometry creation (1-D)

Let’s get familiar with some ways of making 1-D geometries. Bluemira implements functions for the creation of:

  • polygons

  • splines

  • arcs

  • a bit of everything (check geometry.tools module for an extensive list)

Any 1-D geometry is stored in a BluemiraWire object. Just as example, we can start considering a simple linear segmented wire with vertexes on (0,0,0), (1,0,0), and (1,1,0).

points1 = Coordinates({"x": [0, 1, 1], "y": [0, 0, 1], "z": [0, 0, 0]})
first_wire = make_polygon(points1, label="wire1")

# A print of the object will return some useful info
print(first_wire)

# however, each information can be accessed through the respective
# obj property, e.g.
print(f"Wire length: {first_wire.length}")

Concatenation of more wires is also allowed:

points2 = Coordinates({"x": [1, 2], "y": [1, 2], "z": [0, 0]})
second_wire = make_polygon(points2, label="wire2")
full_wire = BluemiraWire([first_wire, second_wire], label="full_wire")
print(full_wire)

In such a case, sub-wires are still accessible as separate entities and can be returned through a search operation on the full wire:

first_wire1 = full_wire.search("wire1")[0]
print(
    f"first_wire and first_wire1 have the same shape: {first_wire1.is_same(first_wire)}"
)

# Simple plot
wire_plotter = plotter.WirePlotter()
wire_plotter.options.view = "xy"
wire_plotter.plot_2d(full_wire)

More complex geometries can be created using splines, arcs, etc.

wires = [
    make_polygon([[0, 3], [0, 0], [0, 0]], label="w1"),
    make_circle(1, (3, 1, 0), 270, 360, label="c2"),
    make_polygon([[4, 4], [1, 3], [0, 0]], label="w3"),
    make_circle(1, (3, 3, 0), 0, 90, label="c4"),
    make_polygon([[3, 0], [4, 4], [0, 0]], label="w5"),
    make_polygon([[0, 0], [4, 0], [0, 0]], label="w6"),
]
closed_wire = BluemiraWire(wires, label="closed_wire")
wire_plotter.plot_2d(closed_wire)

In such a case, the created wire is closed. A check can be done interrogating the is_closed function of the wire:

print(f"wire is closed: {closed_wire.is_closed()}")

Also, note that the sub-wires have been passed in “end-to-start”: the end point of the current wire in the list should match the start point of the next wire in the list.

Geometry creation (2-D and 3-D)

A closed planar 1-D geometry can be used as boundary to generate a 2-D face.

first_face = BluemiraFace(boundary=closed_wire, label="first_face")
print(first_face)

A matplotlib-style plotting of a face can be made similarly to what was done for a wire, i.e. using a FacePlotter

face_plotter = plotter.FacePlotter()
face_plotter.options.view = "xy"
face_plotter.plot_2d(first_face)

If more than one closed wire is given as boundary for a face, the first one is used as the external boundary and subsequent ones are considered as holes.

points = Coordinates({"x": [1, 2, 2, 1], "y": [1, 1, 2, 2]})
hole = make_polygon(points, label="hole", closed=True)
face_with_hole = BluemiraFace(boundary=[closed_wire, hole], label="face_with_hole")
print(face_with_hole)
face_plotter.plot_2d(face_with_hole)

Starting from 1-D or 2-D geometries, 3-D objects can be created, for example, by revolution or extrusion.

first_solid = extrude_shape(face_with_hole, (0, 0, 1), "first_solid")
print(first_solid)

# Note: 3-D operations generate solids that are disconnected from the primitive shape.
# For this reason, it is not possible to retrieve our initial "face_with_hole"
# interrogating "fist_solid".

3-D Display

Geometry objects can be displayed via show_cad, and the appearance of said objects customised by specifying color and transparency.

show_cad(first_solid, DisplayCADOptions(color="blue", transparency=0.1))

Matryoshka structure

Bluemira geometries are structured in a commonly used “Matryoshka” or “Russian doll”-like structure.

Solid -> Shell -> Face -> Wire

These are accessible via the boundary attribute, so, in general, the boundary of a Solid is a Shell or set of Shells, and a Shell will have a set of Faces, etc.

Let’s take a little peek under the hood of our solid:

print(f"Our shape is a BluemiraSolid: {isinstance(first_solid, BluemiraSolid)}")

i, j, k = 0, 0, 0  # This is just to facilitate comprehension
for i, shell in enumerate(first_solid.boundary):
    print(f"Shell: {i}.{j}.{k} is a BluemiraShell: {isinstance(shell, BluemiraShell)}")
    for j, face in enumerate(shell.boundary):
        print(f"Face: {i}.{j}.{k} is a BluemiraFace: {isinstance(face, BluemiraFace)}")
        for k, wire in enumerate(face.boundary):
            print(
                f"Wire: {i}.{j}.{k} is a BluemiraWire: {isinstance(wire, BluemiraWire)}"
            )

Geometric transformations

When applying a geometric transformation to a BluemiraGeo object, that operation is transferred also to the boundary objects (in a recursive manner). That allows consistency between the object shape and its boundary without recreating the boundary set.

Just as example, we are going to apply a translation to our “face_with_hole”.

# To have a reference to the initial object, we make a deepcopy of the face
face_with_hole_copy = face_with_hole.deepcopy("face_copy")

# Now we apply the translation
face_with_hole.translate((6, 1, 0))

# and plot the face before and after the transformation (the translated face
# is plotted in red)
ax = face_plotter.plot_2d(face_with_hole_copy, show=False)
face_plotter.options.face_options["color"] = "red"
face_plotter.plot_2d(face_with_hole, ax=ax, show=False)
plt.title("Translated wire")
plt.show()

# The same happens, for example, to the wire that identifies the hole.
hole_copy = face_with_hole_copy.search("hole")[0]
wire_plotter.options.wire_options["color"] = "black"
ax = wire_plotter.plot_2d(hole_copy, show=False)
wire_plotter.options.wire_options["color"] = "red"
wire_plotter.plot_2d(hole, ax=ax, show=False)
plt.title("Translated wire test")
plt.show()

Geometry creation (complex shapes)

OK, let’s do something more complicated now.

Polygons are good for things with straight lines. Arcs you’ve met already. For everything else, there’s splines.

Say you have a weird shape, that you might calculate via a equation. It’s not a good idea to make a polygon with lots of very small sides for this. It’s computationally expensive, and it will look ugly.

# Spline

x = np.linspace(0, 10, 1000)
y = 0.5 * np.sin(x) + 3 * np.cos(x) ** 2
z = np.zeros(1000)

points = np.array([x, y, z])
spline = interpolate_bspline(points)
points = np.array([x, y + 3, z])
polygon = make_polygon(points)

show_cad(
    [spline, polygon], [DisplayCADOptions(color="blue"), DisplayCADOptions(color="red")]
)
# To get an idea of why polygons are bad / slow / ugly, try:
vector = (0, 0, 1)
show_cad(
    [extrude_shape(spline, vector), extrude_shape(polygon, vector)],
    [DisplayCADOptions(color="blue"), DisplayCADOptions(color="red")],
)

Additional examples

Making 3-D shapes from 2-D shapes

You can:

  • extrude a shape extrude_shape, as we did with our solid

  • revolve a shape revolve_shape

  • sweep a shape sweep_shape

# Make a hollow cylinder, by revolving a rectangle
points = np.array([[4, 5, 5, 4], [0, 0, 0, 0], [2, 2, 3, 3]])
rectangle = BluemiraFace(make_polygon(points, closed=True))

hollow_cylinder = revolve_shape(
    rectangle, base=(0, 0, 0), direction=(0, 0, 1), degree=360
)

show_cad(hollow_cylinder)
# Sweep a profile along a path

points = np.array([[4.5, 4.5], [0, 3], [2.5, 2.5]])
straight_line = make_polygon(points)
quarter_turn = make_circle(center=(3, 3, 2.5), axis=(0, 0, 1), radius=1.5, end_angle=90)
path = BluemiraWire([straight_line, quarter_turn])
solid = sweep_shape(rectangle.boundary[0], path)
show_cad(solid)

Making 3-D shapes from 3-D shapes

Boolean operations often come in very useful when making CAD.

  • You can join geometries together with boolean_fuse

  • You can cut geometries from one another with boolean_cut

points = np.array([[0, 2, 2, 0], [0, 0, 0, 0], [0, 0, 3, 3]])

box_1 = BluemiraFace(make_polygon(points, closed=True))
box_1 = extrude_shape(box_1, (0, 2, 0))

points = np.array([[1, 3, 3, 1], [0, 0, 0, 0], [0, 0, 2, 2]])

box_2 = BluemiraFace(make_polygon(points, closed=True))
box_2 = extrude_shape(box_2, (0, 1, 0))

fused_boxes = boolean_fuse([box_1, box_2])

show_cad(fused_boxes)

cut_box_1 = boolean_cut(box_1, box_2)[0]

show_cad(cut_box_1)

Modification of existing geometries

Now we’re going to look at some stuff that we can do to change geometries we’ve already made.

  • Rotate

  • Translate

  • Scale

# Let's save a deepcopy of a shape before modifying
new_cut_box_1 = cut_box_1.deepcopy()

new_cut_box_1.rotate(base=(0, 0, 0), direction=(0, 1, 0), degree=45)
new_cut_box_1.translate((0, 3, 0))
new_cut_box_1.scale(3)
blue_red_options = [DisplayCADOptions(color="blue"), DisplayCADOptions(color="red")]
show_cad([cut_box_1, new_cut_box_1], options=blue_red_options)

Exporting geometry

Many different CAD file types can be written, for a full list see the CADFileType class.

# Try saving any shape or group of shapes created above
# as a STEP assembly

my_shapes = [cut_box_1]
# Modify this file path to where you want to save the data.
my_file_path = "my_tutorial_assembly.STP"
save_cad(
    my_shapes,
    filename=Path(
        get_bluemira_path("", subfolder="generated_data"), my_file_path
    ).as_posix(),
)