88from pathlib import Path , WindowsPath
99from tempfile import TemporaryDirectory
1010
11+ import numpy as np
1112import trimesh
1213from pydantic import BaseModel , Field
13- from tesseract_core .runtime import Array , Float32
14-
15- # Temporary hardcoded spaceclaim .exe and script files
16- spaceclaim_exe = "F:\\ Ansys installations\\ ANSYS Inc\\ v241\\ scdm\\ SpaceClaim.exe"
17- spaceclaim_script = "geometry_generation.scscript" # Relies on being executed in same directory as tesseract_api.py
18-
19- """
20- Example dict for 8 beam start (s) and end (e) parameters and two z-plane params
21-
22- keyvalues_test = {"__params__.z2": "200",
23- "__params__.z3": "600",
24- "__params__.s1": "0",
25- "__params__.s2": "1 * (math.pi / 8)",
26- "__params__.s3": "2 * (math.pi / 8)",
27- "__params__.s4": "3 * (math.pi / 8)",
28- "__params__.s5": "4 * (math.pi / 8)",
29- "__params__.s6": "5 * (math.pi / 8)",
30- "__params__.s7": "6 * (math.pi / 8)",
31- "__params__.s8": "7 * (math.pi / 8)",
32- "__params__.e1": "(0) + math.pi",
33- "__params__.e2": "(1 * (math.pi / 8)) + math.pi",
34- "__params__.e3": "(2 * (math.pi / 8)) + math.pi",
35- "__params__.e4": "(3 * (math.pi / 8)) + math.pi",
36- "__params__.e5": "(4 * (math.pi / 8)) + math.pi",
37- "__params__.e6": "(5 * (math.pi / 8)) + math.pi",
38- "__params__.e7": "(6 * (math.pi / 8)) + math.pi",
39- "__params__.e8": "(7 * (math.pi / 8)) + math.pi"}
40- """
41-
42-
43- class GridParameters (BaseModel ):
44- z_planes : Array [
45- (None ,),
46- Float32 ,
14+ from tesseract_core .runtime import Array , Differentiable , Float32
15+
16+ # Example spaceclaim .exe and script file Paths
17+ # spaceclaim_exe = "F:\\Ansys installations\\ANSYS Inc\\v241\\scdm\\SpaceClaim.exe"
18+ # spaceclaim_script = "geometry_generation.scscript" # Relies on being executed in same directory as tesseract_api.py
19+
20+ #
21+ # Schemata
22+ #
23+
24+
25+ class InputSchema (BaseModel ):
26+ """Input schema for bar geometry design and SDF generation."""
27+
28+ differentiable_bar_parameters : Differentiable [
29+ Array [
30+ (None , None ),
31+ Float32 ,
32+ ]
4733 ] = Field (
48- description = "Array of Z cutting plane locations" ,
34+ description = (
35+ "Angular positions around the unit circle for the bar geometry. "
36+ "The shape is (num_bars, 2), where num_bars is the number of bars "
37+ "and the second dimension has the start then end location of each bar."
38+ "The final +1 entry represents the two z height coordinates for the cutting plane which combine with "
39+ "a third fixed coordinate centered on the grid with z = grid_height / 2"
40+ )
4941 )
50- beam_starts : Array [
51- (None ,),
52- Float32 ,
42+
43+ differentiable_plane_parameters : Differentiable [
44+ Array [
45+ (None ,),
46+ Float32 ,
47+ ]
5348 ] = Field (
54- description = "Array of beam start angles (radians)" ,
49+ description = (
50+ "Two cutting plane z point heights which combine with a fixed third point "
51+ "centered on the grid at z = grid_height / 2. "
52+ "The shape is (2) "
53+ "The two points are orthognal at the maximum extemts of the grid (+X and +Y)."
54+ )
5555 )
56- beam_ends : Array [
56+
57+ non_differentiable_parameters : Array [
5758 (None ,),
5859 Float32 ,
5960 ] = Field (
60- description = "Array of beam end angles (radians)" ,
61+ description = (
62+ "Flattened array of non-differentiable geometry parameters. "
63+ "The shape is (2), the first float is the maximum height (mm) of the "
64+ "grid (pre z-plane cutting). The second is the beam thickness (mm)."
65+ )
66+ )
67+
68+ """static_parameters: list[int] = Field(
69+ description=(
70+ "List of integers used to construct the geometry. "
71+ "The first integer is the number of bars."
72+ )
73+ )"""
74+
75+ string_parameters : list [str ] = Field (
76+ description = (
77+ "Two string parameters for geometry construction. "
78+ "First str is Path to Spaceclaim executable. "
79+ "Second str is Path to Spaceclaim Script (.scscript)."
80+ )
6181 )
6282
6383
@@ -70,18 +90,87 @@ class TriangularMesh(BaseModel):
7090 )
7191
7292
73- class InputSchema (BaseModel ):
74- grid_parameters : dict = Field (
75- description = "Parameter dictionary defining location of grid beams and Z cutting plane"
76- )
77-
78-
7993class OutputSchema (BaseModel ):
94+ """Output schema for generated geometry and SDF field."""
95+
8096 mesh : TriangularMesh = Field (
81- description = "Output triangular mesh of the grid fin geometry"
97+ description = "Triangular mesh representation of the geometry"
8298 )
8399
84100
101+ #
102+ # Helper functions
103+ #
104+
105+
106+ def build_geometry (
107+ differentiable_bar_parameters : np .ndarray ,
108+ differentiable_plane_parameters : np .ndarray ,
109+ non_differentiable_parameters : np .ndarray ,
110+ string_parameters : list [str ],
111+ ) -> list [trimesh .Trimesh ]:
112+ """Build a Spaceclaim geometry from the parameters by modifying template .scscript.
113+
114+ Return a TriangularMesh object.
115+ """
116+ spaceclaim_exe = Path (string_parameters [0 ])
117+ spaceclaim_script = Path (string_parameters [1 ])
118+
119+ # TODO: Want to stop using TemporaryDirectory for spaceclaim script
120+ # and instead use the unique run directory created everytime the
121+ # tesseract is run (so there is history).
122+ with TemporaryDirectory () as temp_dir :
123+ prepped_script_path , output_file = _prep_scscript (
124+ temp_dir ,
125+ spaceclaim_script ,
126+ differentiable_bar_parameters ,
127+ differentiable_plane_parameters ,
128+ non_differentiable_parameters ,
129+ )
130+ run_spaceclaim (spaceclaim_exe , prepped_script_path )
131+
132+ mesh = trimesh .load (output_file )
133+
134+ return mesh
135+
136+
137+ def _prep_scscript (
138+ temp_dir : TemporaryDirectory ,
139+ spaceclaim_script : Path ,
140+ differentiable_bar_parameters : np .ndarray ,
141+ differentiable_plane_parameters : np .ndarray ,
142+ non_differentiable_parameters : np .ndarray ,
143+ ) -> list [str ]:
144+ """Take tesseract inputs and place into a temp .scscript that will be used to run Spaceclaim.
145+
146+ Return the Path location of this script and the output .stl
147+ """
148+ # Define output file name and location
149+ # TODO: Same as before: can we output grid_fin.stl in the tesseract
150+ # unique run directory instead of temp dir to keep history?
151+ output_file = os .path .join (temp_dir , "grid_fin.stl" )
152+ prepped_script_path = os .path .join (temp_dir , os .path .basename (spaceclaim_script ))
153+ shutil .copy (spaceclaim_script , prepped_script_path )
154+
155+ # Define dict used to input params to .scscript
156+ keyvalues = {}
157+ keyvalues ["__output__" ] = output_file
158+ keyvalues ["__params__.z2" ] = str (differentiable_plane_parameters [0 ])
159+ keyvalues ["__params__.z3" ] = str (differentiable_plane_parameters [1 ])
160+ keyvalues ["__params__.height" ] = non_differentiable_parameters [0 ]
161+ keyvalues ["__params__.thickness" ] = non_differentiable_parameters [1 ]
162+
163+ num_of_bars = len (differentiable_bar_parameters )
164+
165+ for i in range (num_of_bars ):
166+ keyvalues [f"__params__.s{ i + 1 } " ] = str (differentiable_bar_parameters [i ][0 ])
167+ keyvalues [f"__params__.e{ i + 1 } " ] = str (differentiable_bar_parameters [i ][1 ])
168+
169+ _find_and_replace_keys_in_archive (prepped_script_path , keyvalues )
170+
171+ return [prepped_script_path , output_file ]
172+
173+
85174def _safereplace (filedata : str , key : str , value : str ) -> str :
86175 # ensure double backspace in windows path
87176 if isinstance (value , WindowsPath ):
@@ -131,13 +220,19 @@ def _find_and_replace_keys_in_archive(file: Path, keyvalues: dict) -> None:
131220 )
132221
133222
134- def run_spaceclaim (spaceclaim_exe : Path , spaceclaim_script : Path ):
223+ def run_spaceclaim (spaceclaim_exe : Path , spaceclaim_script : Path ) -> None :
224+ """Runs Spaceclaim subprocess with .exe and script Path locations.
225+
226+ Returns the subprocess return code as a placeholder.
227+ """
135228 env = os .environ .copy ()
136229 cmd = str (
137230 f'"{ spaceclaim_exe } " /UseLicenseMode=True /Welcome=False /Splash=False '
138231 + f'/RunScript="{ spaceclaim_script } " /ExitAfterScript=True /Headless=True'
139232 )
140233
234+ # TODO: Not very robust, should probably try use some error handling
235+ # or timeout logic to prevent stalling if Spaceclaim fails.
141236 result = subprocess .run (
142237 cmd ,
143238 shell = True ,
@@ -156,31 +251,20 @@ def run_spaceclaim(spaceclaim_exe: Path, spaceclaim_script: Path):
156251
157252
158253def apply (inputs : InputSchema ) -> OutputSchema :
159- """Create a Spaceclaim geometry based on input parameters and export as a .stl."""
160- cwd = os .getcwd ()
161- output_file = os .path .join (cwd , "grid_fin.stl" )
162-
163- # TODO: lets make sure the parameters are transformed into a correct dict to be used in the script
164- keyvalues = inputs .grid_parameters .copy ()
165- keyvalues ["__output__" ] = output_file
166-
167- with TemporaryDirectory () as temp_dir :
168- # Copy spaceclaim template script to temp dir
169- copied_file_path = os .path .join (temp_dir , os .path .basename (spaceclaim_script ))
170- shutil .copy (spaceclaim_script , copied_file_path )
171-
172- # Update temp spaceclaim script and use to generate .stl
173- # update_script = _find_and_replace_keys_in_archive(copied_file_path, keyvalues)
174- # spaceclaim_result = run_spaceclaim(spaceclaim_exe, copied_file_path)
254+ """Create a Spaceclaim geometry based on input parameters.
255+
256+ Returns TraingularMesh obj and exports a .stl.
257+ """
258+ mesh = build_geometry (
259+ differentiable_bar_parameters = inputs .differentiable_bar_parameters ,
260+ differentiable_plane_parameters = inputs .differentiable_plane_parameters ,
261+ non_differentiable_parameters = inputs .non_differentiable_parameters ,
262+ string_parameters = inputs .string_parameters ,
263+ )
175264
176- # TODO: lets read the .stl file using trimesh
177- mesh = trimesh .load (output_file )
178265 return OutputSchema (
179266 mesh = TriangularMesh (
180- points = Array ( mesh .vertices .astype (" float32" ) ),
181- faces = Array ( mesh .faces .astype ("float32" ) ),
267+ points = mesh .vertices .astype (np . float32 ),
268+ faces = mesh .faces .astype (np . int32 ),
182269 )
183270 )
184- # return OutputSchema(
185- # placeholder_output=f"Subprocess return code: {spaceclaim_result}"
186- # )
0 commit comments