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 solidrevolve a shape
revolve_shapesweep 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_fuseYou 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(),
)