Coverage for /opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/pysagas/optimisation/cart3d/utilities.py: 0%
163 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-30 04:27 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-30 04:27 +0000
1import os
2import time
3import shutil
4from random import random
5from typing import Optional
6from hypervehicle.utilities import append_sensitivities_to_tri
9class C3DPrep:
10 def __init__(
11 self,
12 logfile,
13 jitter_denom: float = 1000,
14 rotation_attempts: int = 6,
15 info_file: str = None,
16 write_config: bool = True,
17 ) -> None:
18 self._logfile = logfile
19 self._info = info_file if info_file is not None else self._logfile
20 self._jitter_denom = jitter_denom # for 1000; Max of 0.0001, min of 0
21 self._rotation_attempts = rotation_attempts
22 self._write_config = write_config
24 def _run_stl2tri(self, stl_files: list):
25 tri_files = []
26 for file in stl_files:
27 prefix = file.split(".")[0]
28 tri_file = prefix + ".tri"
29 os.system(f"stl2tri.pl {file} {tri_file} >> {self._logfile} 2>&1")
30 tri_files.append(tri_file)
32 os.system(f"rm *.comp.tri *.off >> {self._logfile} 2>&1")
33 return tri_files
35 def _jitter_tri_files(self, tri_files):
36 for file in tri_files:
37 prefix = file.split(".")[0]
38 x_pert = random() / self._jitter_denom
39 y_pert = random() / self._jitter_denom
40 z_pert = random() / self._jitter_denom
41 os.system(
42 f"trix -x {x_pert} -y {y_pert} -z {z_pert} -o {prefix} {file} >> {self._logfile} 2>&1"
43 )
45 def _shift_all(
46 self,
47 tri_files,
48 x_shift,
49 y_shift,
50 z_shift,
51 component: str = None,
52 reverse: bool = False,
53 ):
54 if component is None:
55 transform_files = tri_files
56 else:
57 transform_files = [component]
59 for file in transform_files:
60 prefix = ".".join(file.split(".")[:-1])
61 if reverse:
62 os.system(
63 f"trix -x {-x_shift} -y {-y_shift} -z {-z_shift} -o {prefix} {file} >> {self._logfile} 2>&1"
64 )
65 else:
66 os.system(
67 f"trix -x {x_shift} -y {y_shift} -z {z_shift} -o {prefix} {file} >> {self._logfile} 2>&1"
68 )
70 def _rotate_all(
71 self,
72 tri_files,
73 x_rot,
74 y_rot,
75 z_rot,
76 component: str = None,
77 reverse: bool = False,
78 ):
79 # Define rotations dict
80 rotations = {
81 "x": x_rot,
82 "y": y_rot,
83 "z": z_rot,
84 }
86 # Determine files to be transformed
87 if component is None:
88 # Rotate all .tri files (as provided)
89 transform_files = tri_files
90 else:
91 # Rotate the single component
92 transform_files = [component]
94 for file in transform_files:
95 prefix = ".".join(file.split(".")[:-1])
97 # Check order of operations
98 if reverse:
99 order = ["z", "y", "x"]
100 else:
101 order = ["x", "y", "z"]
103 # Apply rotations
104 for axis in order:
105 rotation = rotations[axis]
106 os.system(
107 f"trix -r{axis} {rotation} -o {prefix} {file} >> {self._logfile} 2>&1"
108 )
110 def _run_comp2tri(self, tri_files):
111 tri_files_str = " ".join(tri_files)
113 if os.path.exists("Config.xml"):
114 # Remove old config file
115 os.remove("Config.xml")
117 # Run command
118 conf_str = "-config" if self._write_config else ""
119 os.system(
120 f"comp2tri -makeGMPtags {tri_files_str} {conf_str} >> {self._logfile} 2>&1"
121 )
123 if self._write_config:
124 # Await Config.xml
125 while not os.path.exists("Config.xml"):
126 time.sleep(0.2)
128 # Overwrite Config.xml Component names using tri files
129 original = os.path.join("Config.xml")
130 new = os.path.join("temp_config.xml")
131 with open(new, "w+") as new_file:
132 with open(original, "r") as original_file:
133 for line in original_file:
134 if "Component Name" in line:
135 # Get component number
136 name_prefix = "Component_"
137 name_start = line.index(name_prefix)
138 comp_no = line.split('"')[1].split("_")[-1]
139 tri_prefix = tri_files[int(comp_no) - 1].split(".")[0]
141 # Update line
142 line = (
143 line[:name_start]
144 + tri_prefix
145 + line[name_start + len(name_prefix) + 1 :]
146 )
148 # Write line to new file
149 new_file.write(line)
151 # # Replace original aero.csh file with updated file
152 shutil.copymode(original, new)
153 os.remove(original)
154 os.rename(new, original)
156 else:
157 # Await Components.tri
158 while not os.path.exists("Components.tri"):
159 time.sleep(0.2)
161 def _run_intersect(self):
162 os.system(f"intersect >> {self._logfile} 2>&1")
164 @staticmethod
165 def _get_stl_files() -> list[str]:
166 path = os.getcwd()
167 all_files = [
168 f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))
169 ]
170 stl_files = []
171 for file in all_files:
172 if file.split(".")[-1] == "stl":
173 stl_files.append(file)
175 # Sort files
176 stl_files.sort()
178 return stl_files
180 @staticmethod
181 def _check_for_success():
182 success = False
183 if os.path.isfile("Components.i.tri"):
184 success = True
185 return success
187 def intersect_stls(self, stl_files: Optional[list[str]] = None) -> bool:
188 """Create Components.i.tri by intersecting all STL files."""
189 # Check for existing intersected file
190 if self._check_for_success():
191 self._log("Intersected components file already present.")
192 return True
194 # Continue
195 if not stl_files:
196 stl_files = self._get_stl_files()
197 tri_files = self._run_stl2tri(stl_files)
199 # First try intersect original files
200 self._log("Making first attempt to intersect components with no perturbations.")
201 self._run_comp2tri(tri_files)
202 self._run_intersect()
203 successful = self._check_for_success()
204 if successful:
205 self._log("Success.")
206 return True
208 # That failed, try jittering components
209 self._log("Attempt failed, now attempting to intersect jittered components.")
210 self._jitter_tri_files(tri_files)
211 self._run_comp2tri(tri_files)
212 self._run_intersect()
213 successful = self._check_for_success()
214 if successful:
215 self._log("Success.")
216 return True
218 # That failed, try arbitrary shifts away
219 self._log("Attempt failed, now attempting random perturbations.")
220 for attempt in range(self._rotation_attempts):
221 # Define shifts
222 x_shift = random() * 10 # Max of 10, min of 0
223 y_shift = random() * 10 # Max of 10, min of 0
224 z_shift = random() * 10 # Max of 10, min of 0
226 # Define rotations
227 x_rot = random() * 10 # Max of 10, min of 0
228 y_rot = random() * 10 # Max of 10, min of 0
229 z_rot = random() * 10 # Max of 10, min of 0
231 # Apply transformations
232 self._shift_all(
233 tri_files=tri_files, x_shift=x_shift, y_shift=y_shift, z_shift=z_shift
234 )
235 self._rotate_all(tri_files=tri_files, x_rot=x_rot, y_rot=y_rot, z_rot=z_rot)
237 if attempt > 0:
238 # Also jitter
239 self._log(f"On attempt {attempt+1}, also applying random jitter.")
240 self._jitter_tri_files(tri_files)
242 # Make intersect attempt
243 self._run_comp2tri(tri_files)
244 self._run_intersect()
245 successful = self._check_for_success()
247 if successful:
248 # Move configuration back to original location
249 self._log(
250 "Success. Now moving Components.i.tri back to original position."
251 )
252 self._rotate_all(
253 tri_files=tri_files,
254 x_rot=-x_rot,
255 y_rot=-y_rot,
256 z_rot=-z_rot,
257 component="Components.i.tri",
258 reverse=True, # Perform in reverse order
259 )
260 self._shift_all(
261 tri_files=tri_files,
262 x_shift=x_shift,
263 y_shift=y_shift,
264 z_shift=z_shift,
265 component="Components.i.tri",
266 reverse=True,
267 )
268 return True
270 else:
271 # Need to reset tri files
272 self._log(
273 f"Random perturbation attempt {attempt+1} failed. Resetting "
274 + ".tri files and attempting again."
275 )
276 tri_files = self._run_stl2tri(stl_files)
278 # Finish log
279 self._log("Unsuccessful.")
281 return False
283 def run_autoinputs(self, r: int = 2, *args):
284 """Runs autoInputs to create input.c3d.
286 Parameters
287 -----------
288 r : int, optional
289 The distance to farfield normalized by max geometry size. The default is 2.
290 args : str, optional
291 Any extra arguments to provide to autoInputs, as strings. For arguments with
292 values, just include the value in the string.
293 """
294 extra_args = "-" + " -".join(args) if args else ""
295 os.system(f"autoInputs -r 2 {extra_args} >> {self._logfile} 2>&1")
297 def run_aero(self):
298 """Runs aero.csh."""
299 os.system("./aero.csh restart")
301 def _log(self, msg: str):
302 with open(self._info, "a") as f:
303 f.write("\nC3DPrep: " + msg)
306def combine_sense_data(
307 components_filepath: str,
308 sensitivity_files: list[str],
309 match_target: float = 0.9,
310 tol_0: float = 1e-5,
311 max_tol: float = 1e-1,
312 outdir: Optional[str] = None,
313 verbosity: Optional[int] = 1,
314):
315 """Combine the individual component sensitivity data with the
316 intersected geometry file (eg. Components.i.tri).
317 """
318 match_frac = 0
319 tol = tol_0
320 while match_frac < match_target:
321 # Check tolerance
322 if tol > max_tol:
323 raise Exception(
324 "Cannot combine sensitivity data (match fraction: "
325 + f"{match_frac}, tolerance: {tol}, max tolerance: {max_tol})."
326 )
328 # Run matching algorithm
329 match_frac = append_sensitivities_to_tri(
330 dp_filenames=sensitivity_files,
331 components_filepath=components_filepath,
332 match_tolerance=tol,
333 verbosity=verbosity,
334 outdir=outdir,
335 )
337 if match_frac < match_target and verbosity > 0:
338 print(
339 "Failed to combine sensitivity data "
340 f"({100*match_frac:.02f}% match rate)."
341 )
342 print(f" Increasing matching tolerance to {tol*10} and trying again.")
343 elif verbosity > 0:
344 print(
345 "Component sensitivity data matched to intersected geometry "
346 + f"with {100*match_frac:.02f}% match rate."
347 )
349 # Increase matching tolerance
350 tol *= 10