Wrapping SpaceClaim as a Tesseract¶
This guide outlines how to wrap Ansys SpaceClaim as a Tesseract. For this, we will use non-containerized execution to start an HTTP server that dispatches requests to SpaceClaim through SpaceClaim scripts (.scscript).
See also
The full code for this Tesseract can be found under demo/_showcase/ansys-shapeopt/spaceclaim in the Tesseract Core repository.
The Tesseract can be seen in action within our rocket fin optimization showcase.
Why SpaceClaim as a Tesseract?¶
Complex CAD models imported from parametric CAD software often require pre-processing before they can be fed into a simulator, such as extracting a fluid volume or naming domain faces such that appropriate boundary conditions can be applied.
SpaceClaim is commonly used to generate parametric geometries and perform pre-processing actions on them. In this example we demonstrate the use of SpaceClaim as a geometry engine within Tesseract-driven processing pipelines. This unlocks powerful applications operating on real-world CAD geometries.
Architecture of the SpaceClaim Tesseract implemented here.¶
Core concepts¶
Folder structure¶
When creating a new Tesseract you should have a directory with three files like so:
$ tesseract init --name spaceclaim --target-dir ./spaceclaim
$ tree ./spaceclaim
spaceclaim
├── tesseract_api.py
├── tesseract_config.yaml
└── tesseract_requirements.txt
See also
If this doesn’t look familiar you can learn more about Tesseract basics here.
To wrap SpaceClaim as a Tesseract, we will have to implement each of these files:
tesseract_api.pycontains all logic on accepted inputs / outputs and dispatches calls to SpaceClaim.tesseract_config.yamlis not relevant here, since we will be invoking the Tesseract without containerization (see below).tesseract_requirements.txtcontains additional Python requirements to run the Tesseract.
Non-containerized usage via tesseract-runtime¶
Note
Tesseract without containerization — Tesseracts are most commonly used in the form of Docker containers. This is not a neccessary requirement, and any object that adheres to the Tesseract interface is a valid Tesseract (see also Tesseracts without containerization).
SpaceClaim is typically running directly on a Windows host machine, so instead of using Docker to build an image and spin up a container, we leverage the Tesseract runtime CLI to serve the SpaceClaim Tesseract on bare metal and expose SpaceClaim’s functionality over HTTP.
So instead of using the more common tesseract build, we install and use the tesseract-runtime CLI application which will provide us an interface with the Tesseract:
$ pip install tesseract-core[runtime]
Warning
Windows is officially only supported via Windows Subsystem for Linux (WSL), see Windows support. Make sure to use an appropriate WSL setup when running into issues.
Now with an a open port of your choice, and from within the Tesseract directory, we can execute:
$ pip install ./tesseract-requirements.txt
$ tesseract-runtime serve --host 0.0.0.0 --port port_number
The result is a Tesseract Runtime Server.
$ tesseract-runtime serve --host 0.0.0.0 --port 443
INFO: Started server process [14888]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:443 (Press CTRL+C to quit)
Implementing the Tesseract¶
tesseract_requirements.txt¶
Besides a SpaceClaim installation and a recent Python version, this Tesseract only requires one dependency, the Python package trimesh. We add it to tesseract_requirements.txt:
trimesh==4.9.0
Since we are running the Tesseract in a non-containerized way, all requirements need to be installed into the current Python interpreter:
$ pip install -r tesseract_requirements.txt
tesseract_api.py — Input and output schema¶
When using SpaceClaim as a geometry engine, the goal is typically to map design parameters in the parametric CAD model to a surface mesh. Here, we are creating a SpaceClaim Tesseract that operates on a grid fin geometry with a number of parameters representing the position of bars and their thickness.
Note
This particular choice of inputs and outputs is motivated in our rocket fin optimization showcase.
Input schema¶
This Tesseract accepts multiple goemetry parameters to create N grid fin geometries simulatanously. That way, we hide the startup latency of SpaceClaim when requesting a large number of geometries.
The InputSchema class looks like this:
class InputSchema(BaseModel):
"""Input schema for bar geometry design and SDF generation."""
differentiable_parameters: list[
Array[
(None,),
Float32,
]
] = Field(
description=(
"Angular positions around the unit circle for the bar geometry. "
"The shape is (num_bars+1, 2), where num_bars is the number of bars "
"and the second dimension has the start then end location of each bar."
"The first (+1) entry represents the two z height coordinates for the cutting plane which combine with "
"a third fixed coordinate centered on the grid with z = grid_height / 2"
)
)
non_differentiable_parameters: list[
Array[
(None,),
Float32,
]
] = Field(
description=(
"Flattened array of non-differentiable geometry parameters. "
"The shape is (2), the first float is the maximum height (mm) of the "
"grid (pre z-plane cutting). The second is the beam thickness (mm)."
)
)
static_parameters: list[list[int]] = Field(
description=("List of integers used to construct the geometry."),
default=[],
)
string_parameters: list[str] = Field(
description=(
"Two string parameters for geometry construction. "
"First str is Path to SpaceClaim executable. "
"Second str is Path to SpaceClaim Script (.scscript)."
)
)
Output schema¶
The output of the Tesseract is a list of TriangularMesh objects representing the N grid fin meshes:
class OutputSchema(BaseModel):
"""Output schema for generated geometry."""
meshes: list[TriangularMesh] = Field(
description="Triangular meshes representing the geometries"
)
class TriangularMesh(BaseModel):
"""Triangular mesh representation with fixed-size arrays."""
points: Array[(None, 3), Float32] = Field(description="Array of vertex positions.")
faces: Array[(None, 3), Float32] = Field(
description="Array of triangular faces defined by indices into the points array."
)
tesseract_api.py — apply¶
The apply function that we are invoking with the above command builds each of the grid fin geometries and extracts the mesh data from the trimesh objects.
def apply(inputs: InputSchema) -> OutputSchema:
"""Create a SpaceClaim geometry based on input parameters.
Returns TraingularMesh objects.
"""
trimeshes = build_geometries(
differentiable_parameters=inputs.differentiable_parameters,
non_differentiable_parameters=inputs.non_differentiable_parameters,
static_parameters=inputs.static_parameters,
string_parameters=inputs.string_parameters,
)
return OutputSchema(
meshes=[
TriangularMesh(
points=mesh.vertices.astype(np.float32),
faces=mesh.faces.astype(np.int32),
)
for mesh in trimeshes
]
)
To build the geometries we first prepare the SpaceClaim .scscript by replacing placeholder values with the user inputs via string substitution. SpaceClaim is then run, outputting .stl meshes that are read with trimesh.
def build_geometries(
differentiable_parameters: list[np.ndarray],
non_differentiable_parameters: list[np.ndarray],
static_parameters: list[list[int]],
string_parameters: list[str],
) -> list[trimesh.Trimesh]:
"""Build SpaceClaim geometries from the parameters by modifying template .scscript.
Returns a list of trimesh objects.
"""
spaceclaim_exe = Path(string_parameters[0])
spaceclaim_script = Path(string_parameters[1])
with TemporaryDirectory() as temp_dir:
prepped_script_path = _prep_scscript(
temp_dir,
spaceclaim_script,
differentiable_parameters,
non_differentiable_parameters,
)
run_spaceclaim(spaceclaim_exe, prepped_script_path)
meshes = []
for output_stl in sorted(Path(temp_dir).glob("*.stl")):
mesh = trimesh.load(output_stl)
meshes.append(mesh)
return meshes
The .scscript preperation is unique to this grid fin example, with the user input values being processed into dictionaries that are then used within the string substitution. For a different geometry one would have to create their own .scscript and dictionaries with all the neccessary inputs required.
def _prep_scscript(
temp_dir: TemporaryDirectory,
spaceclaim_script: Path,
differentiable_parameters: list[np.ndarray],
non_differentiable_parameters: list[np.ndarray],
) -> list[str]:
"""Take Tesseract inputs and place into a temp .scscript that will be used to run SpaceClaim.
Return the Path location of this script and the output .stl.
"""
# Define output file name and location
output_file = os.path.join(
temp_dir, "grid_fin"
) # .stl ending is included in .scscript
prepped_script_path = os.path.join(temp_dir, os.path.basename(spaceclaim_script))
shutil.copy(spaceclaim_script, prepped_script_path)
# Define dict used to input params to .scscript
# Converts np.float32 to python floats so string substitution is clean
keyvalues = {}
keyvalues["__output__"] = output_file
keyvalues["__params__.zeds"] = [
[float(geom_params[0]), float(geom_params[1])]
for geom_params in differentiable_parameters
]
keyvalues["__params__.height"] = non_differentiable_parameters[0][0]
keyvalues["__params__.thickness"] = non_differentiable_parameters[0][1]
num_of_batches = len(differentiable_parameters) # number of geometries requested
num_of_bars = (
len(differentiable_parameters[0]) - 2
) // 2 # Use first geometry in batch to test number of beams
assert num_of_bars == 8
batch_starts = []
batch_ends = []
for i in range(num_of_batches):
geom_starts = []
geom_ends = []
for j in range(num_of_bars):
geom_starts.append(float(differentiable_parameters[i][j * 2 + 2]))
geom_ends.append(float(differentiable_parameters[i][j * 2 + 3]))
batch_starts.append(geom_starts)
batch_ends.append(geom_ends)
keyvalues["__params__.starts"] = (
batch_starts # convert to string to ease injection into file text
)
keyvalues["__params__.ends"] = batch_ends
_find_and_replace_keys_in_archive(prepped_script_path, keyvalues)
return prepped_script_path
def _find_and_replace_keys_in_archive(file: Path, keyvalues: dict) -> None:
# work on the zip in a temporary directory
with TemporaryDirectory() as temp_dir:
# extract zip
with zipfile.ZipFile(file, "r") as zip_ref:
zip_ref.extractall(temp_dir)
# walk through the extracted files/folders
for foldername, _subfolders, filenames in os.walk(temp_dir):
for filename in filenames:
# read in file
filepath = os.path.join(foldername, filename)
try:
with open(filepath) as f:
filedata = f.read()
except Exception:
filedata = None
# find/replace
if filedata:
for key, value in keyvalues.items():
if value is not None:
filedata = _safereplace(filedata, key, value)
# write to file
with open(filepath, "w") as f:
f.write(filedata)
# write out all files back to zip
with zipfile.ZipFile(file, "w") as zip_ref:
for foldername, _subfolders, filenames in os.walk(temp_dir):
for filename in filenames:
filepath = os.path.join(foldername, filename)
zip_ref.write(
filepath,
arcname=os.path.relpath(filepath, start=temp_dir),
)
def _safereplace(filedata: str, key: str, value: str) -> str:
# ensure double backspace in windows path
if isinstance(value, WindowsPath):
value = str(value)
value = value.replace("\\", "\\\\")
else:
value = str(value)
return filedata.replace(key, value)
Once the .scscript is ready the final step is to run SpaceClaim. Here it is easy to see how this process could be extended to any software that is running on the host machine. For example Ansys Fluent could also be wrapped in a runtime Tesseract, potentially using an adjoint solver to produce gradient information, allowing the Tesseract to be differentiable.
def run_spaceclaim(spaceclaim_exe: Path, spaceclaim_script: Path) -> None:
"""Runs SpaceClaim subprocess with .exe and script Path locations.
Returns the subprocess return code.
"""
env = os.environ.copy()
cmd = str(
f'"{spaceclaim_exe}" /UseLicenseMode=True /Welcome=False /Splash=False '
+ f'/RunScript="{spaceclaim_script}" /ExitAfterScript=True /Headless=True'
)
result = subprocess.run(
cmd,
shell=True,
check=False,
capture_output=True,
text=True,
env=env,
)
return result.returncode
Invoking the Tesseract¶
Now that we have defined the Tesseract we can use it. From within the Tesseract’s root directory, start the runtime server with a port of your choice:
$ tesseract-runtime serve --host 0.0.0.0 --port 443
We can now test the Tesseract manually by sending an HTTP request for two grid fin geometries either from the same computer, as shown here, or via the network. Make sure to change the URL IP and port to reflect your setup, along with the SpaceClaim.exe path:
# Bash
$ curl -d '{
"inputs": {
"differentiable_parameters": [
[200, 600, 0, 3.14, 0.39, 3.53, 0.79, 3.93, 1.18, 4.32, 1.57, 4.71, 1.96, 5.11, 2.36, 5.50, 2.75, 5.89],
[400, 400, 0, 3.14, 0.39, 3.53, 0.79, 3.93, 1.18, 4.32, 1.57, 4.71, 1.96, 5.11, 2.36, 5.50, 2.75, 5.89]
],
"non_differentiable_parameters": [
[800, 100],
[800, 100]
],
"string_parameters": [
"F:\\Ansys installations\\ANSYS Inc\\v241\\scdm\\SpaceClaim.exe",
"geometry_generation.scscript"
]
}
}' \
-H "Content-Type: application/json" \
http://127.0.0.1:443/apply
Or:
# Windows PowerShell
curl -Method POST `
-Uri "http://127.0.0.1:443/apply" `
-ContentType "application/json" `
-Body '{"inputs":{"differentiable_parameters":[[200,600,0,3.14,0.39,3.53,0.79,3.93,1.18,4.32,1.57,4.71,1.96,5.11,2.36,5.50,2.75,5.89],[400,400,0,3.14,0.39,3.53,0.79,3.93,1.18,4.32,1.57,4.71,1.96,5.11,2.36,5.50,2.75,5.89]],"non_differentiable_parameters":[[800,100],[800,100]],"string_parameters":["F:\\Ansys installations\\ANSYS Inc\\v241\\scdm\\SpaceClaim.exe","geometry_generation.scscript"]}}'
After about ~15 seconds the mesh output is returned and displayed in text form in your terminal. The point coordinates and cells correspond to a grid fin like below (shown with randomised cross beam locations).
Grid fin geometry shown with randomised beam locations.¶
Next steps¶
Invoking SpaceClaim via HTTP is only the start of the Tesseract journey.
For example, by using finite difference approximations under the hood, we can make the resulting geometry differentiable with respect to the design parameters. For a concrete demonstration of end-to-end shape optimization in action, please have a look at our rocket fin optimization showcase.