Skip to content

Commit 95524b2

Browse files
authored
Update of FAST.Farm for windows (#63)
* Update of FAST.Farm for windows * Fixing unittests
1 parent 0b92e95 commit 95524b2

File tree

20 files changed

+4858
-688
lines changed

20 files changed

+4858
-688
lines changed

data/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.lin
2+
*.sum.yaml

openfast_toolbox/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from .io.fast_input_deck import FASTInputDeck
99

1010
# Add version to package
11-
with open(os.path.join(os.path.dirname(__file__), "..", "VERSION")) as fid:
12-
__version__ = fid.read().strip()
11+
try:
12+
with open(os.path.join(os.path.dirname(__file__), "..", "VERSION")) as fid:
13+
__version__ = fid.read().strip()
14+
except:
15+
__version__='v0.0.0-Unknown'
1316

1417

openfast_toolbox/case_generation/runner.py

Lines changed: 179 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# --- For cmd.py
22
import os
3+
import sys
34
import subprocess
45
import multiprocessing
56

67
import collections
8+
from contextlib import contextmanager
79
import glob
810
import pandas as pd
911
import numpy as np
@@ -14,9 +16,19 @@
1416
# --- Fast libraries
1517
from openfast_toolbox.io.fast_input_file import FASTInputFile
1618
from openfast_toolbox.io.fast_output_file import FASTOutputFile
19+
from openfast_toolbox.tools.strings import FAIL, OK
1720

1821
FAST_EXE='openfast'
1922

23+
@contextmanager
24+
def safe_cd(newdir):
25+
prevdir = os.getcwd()
26+
try:
27+
os.chdir(newdir)
28+
yield
29+
finally:
30+
os.chdir(prevdir)
31+
2032
# --------------------------------------------------------------------------------}
2133
# --- Tools for executing FAST
2234
# --------------------------------------------------------------------------------{
@@ -72,10 +84,10 @@ def _report(p):
7284
# --- Giving a summary
7385
if len(Failed)==0:
7486
if verbose:
75-
print('[ OK ] All simulations run successfully.')
87+
OK('All simulations run successfully.')
7688
return True, Failed
7789
else:
78-
print('[FAIL] {}/{} simulations failed:'.format(len(Failed),len(inputfiles)))
90+
FAIL('{}/{} simulations failed:'.format(len(Failed),len(inputfiles)))
7991
for p in Failed:
8092
print(' ',p.input_file)
8193
return False, Failed
@@ -113,34 +125,178 @@ class Dummy():
113125
p.exe = exe
114126
return p
115127

116-
def runBatch(batchfiles, showOutputs=True, showCommand=True, verbose=True):
128+
def in_jupyter():
129+
try:
130+
from IPython import get_ipython
131+
return 'ipykernel' in str(type(get_ipython()))
132+
except:
133+
return False
134+
135+
def stream_output(std, buffer_lines=5, prefix='|', line_count=True):
136+
if in_jupyter():
137+
from IPython.display import display, update_display
138+
# --- Jupyter mode ---
139+
#handles = [display("DUMMY LINE FOR BUFFER", display_id=True) for _ in range(buffer_lines)]
140+
#buffer = []
141+
#for line in std:
142+
# line = line.rstrip()
143+
# buffer.append(line)
144+
# if len(buffer) > buffer_lines:
145+
# buffer.pop(0)
146+
# # update all display slots
147+
# for i, handle in enumerate(handles):
148+
# text = buffer[i] if i < len(buffer) else ""
149+
# update_display(text, display_id=handle.display_id)
150+
151+
# --- alternative using HTML
152+
from IPython.display import display, update_display, HTML
153+
import html as _html
154+
155+
# --- Jupyter mode with HTML ---
156+
handles = [display(HTML("<pre style='margin:0'>{}DUMMY LINE FOR BUFFER</pre>".format(prefix)), display_id=True)
157+
for _ in range(buffer_lines)]
158+
buffer = []
159+
iLine=0
160+
for line in std:
161+
iLine+=1
162+
line = line.rstrip("\r\n")
163+
if line_count:
164+
line = f"{iLine:>5}: {line}"
165+
line = f"{prefix}{line}"
166+
buffer.append(line)
167+
if len(buffer) > buffer_lines:
168+
buffer.pop(0)
169+
# update all display slots
170+
for i, handle in enumerate(handles):
171+
text = buffer[i] if i < len(buffer) else ""
172+
173+
html_text = "<pre style='margin:0'>{}</pre>".format(_html.escape(text) if text else "&nbsp;")
174+
update_display(HTML(html_text), display_id=handle.display_id)
175+
176+
else:
177+
import shutil
178+
179+
term_width = shutil.get_terminal_size((80, 20)).columns
180+
for _ in range(buffer_lines):
181+
print('DummyLine')
182+
# --- Terminal mode ---
183+
buffer = []
184+
iLine = 0
185+
for line in std:
186+
iLine += 1
187+
line = line.rstrip()
188+
line = line.rstrip()
189+
if line_count:
190+
line = f"{iLine:>5}: {line}"
191+
line = f"{prefix}{line}"
192+
line = line[:term_width] # truncate to fit in one line
193+
buffer.append(line)
194+
if len(buffer) > buffer_lines:
195+
buffer.pop(0)
196+
sys.stdout.write("\033[F\033[K" * len(buffer))
197+
for l in buffer:
198+
print(l)
199+
sys.stdout.flush()
200+
201+
def stdHandler(std, method='show'):
202+
from collections import deque
203+
import sys
204+
205+
if method =='show':
206+
for line in std:
207+
print(line, end='')
208+
return None
209+
210+
elif method =='store':
211+
return std.read() # read everything
212+
213+
elif method.startswith('buffer'):
214+
buffer_lines = int(method.split('_')[1])
215+
buffer = deque(maxlen=buffer_lines)
216+
print('------ Beginning of buffer outputs ----------------------------------')
217+
stream_output(std, buffer_lines=buffer_lines)
218+
print('------ End of buffer outputs ----------------------------------------')
219+
return None
220+
221+
222+
223+
224+
225+
def runBatch(batchfiles, showOutputs=True, showCommand=True, verbose=True, newWindow=False, closeWindow=True, shell_cmd='bash', nBuffer=0):
117226
"""
118227
Run one or several batch files
119228
TODO: error handling, status, parallel
229+
230+
showOutputs=True => stdout & stderr printed live
231+
showOutputs=False => stdout captured internally, stderr printed live
232+
233+
For output to show in a Jupyter notebook, we cannot use stdout=None, or stderr=None, we need to use Pipe
234+
120235
"""
236+
import sys
237+
windows = (os.name == "nt")
121238

122239
if showOutputs:
123240
STDOut= None
241+
std_method = 'show'
242+
if nBuffer>0:
243+
std_method=f'buffer_{nBuffer}'
124244
else:
125-
STDOut= open(os.devnull, 'w')
126-
127-
curDir = os.getcwd()
128-
print('Current directory', curDir)
245+
std_method = 'store'
246+
#STDOut= open(os.devnull, 'w')
247+
#STDOut= subprocess.DEVNULL
248+
STDOut= subprocess.PIPE
129249

130250
def runOneBatch(batchfile):
131251
batchfile = batchfile.strip()
132252
batchfile = batchfile.replace('\\','/')
133253
batchDir = os.path.dirname(batchfile)
254+
batchfileRel = os.path.relpath(batchfile, batchDir)
255+
if windows:
256+
command = [batchfileRel]
257+
else:
258+
command = [shell_cmd, batchfileRel]
259+
134260
if showCommand:
135-
print('>>>> Running batch file:', batchfile)
136-
print(' in directory:', batchDir)
137-
try:
138-
os.chdir(batchDir)
139-
returncode=subprocess.call([batchfile], stdout=STDOut, stderr=subprocess.STDOUT, shell=shell)
140-
except:
141-
os.chdir(curDir)
142-
returncode=-10
143-
return returncode
261+
print('[INFO] Running batch file:', batchfileRel)
262+
print(' using command:', command)
263+
print(' in directory:', batchDir)
264+
265+
if newWindow:
266+
# --- Launch a new window (windows only for now)
267+
if windows:
268+
cmdflag= '/c' if closeWindow else '/k'
269+
subprocess.Popen(f'start cmd {cmdflag} {batchfileRel}', shell=True, cwd=batchDir)
270+
return 0
271+
else:
272+
raise NotImplementedError('Running batch in `newWindow` only implemented on Windows.')
273+
else:
274+
# --- We wait for outputs
275+
stdout_data = None
276+
with safe_cd(batchDir): # Automatically go back to current directory
277+
try:
278+
# --- Option 2
279+
# Use Popen so we can print outputs live
280+
#proc = subprocess.Popen([batchfileRel], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell, text=True )
281+
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, text=True )
282+
# Print or store stdout
283+
stdout_data = stdHandler(proc.stdout, method=std_method)
284+
# Always print errors output line by line
285+
for line in proc.stderr:
286+
print(line, end='')
287+
proc.wait()
288+
returncode = proc.returncode
289+
# Dump stdout if there was an error
290+
if returncode != 0 and stdout_data:
291+
print("\n--- Captured stdout ---")
292+
print(stdout_data)
293+
except FileNotFoundError as e:
294+
print('[FAIL] Running Batch failed, a file or command was not found see below:\n'+str(e))
295+
returncode=-10
296+
except Exception as e:
297+
print('[FAIL] Running Batch failed, see below:\n'+str(e))
298+
returncode=-10
299+
return returncode
144300

145301
shell=False
146302
if isinstance(batchfiles,list):
@@ -152,20 +308,20 @@ def runOneBatch(batchfile):
152308
Failed.append(batchfile)
153309
if len(Failed)>0:
154310
returncode=1
155-
print('[FAIL] {}/{} Batch files failed.'.format(len(Failed),len(batchfiles)))
311+
FAIL('{}/{} Batch files failed.'.format(len(Failed),len(batchfiles)))
156312
print(Failed)
157313
else:
158314
returncode=0
159315
if verbose:
160-
print('[ OK ] {} batch filse ran successfully.'.format(len(batchfiles)))
316+
OK('{} batch files ran successfully.'.format(len(batchfiles)))
161317
# TODO
162318
else:
163319
returncode = runOneBatch(batchfiles)
164320
if returncode==0:
165321
if verbose:
166-
print('[ OK ] Batch file ran successfully.')
322+
OK('Batch file ran successfully.')
167323
else:
168-
print('[FAIL] Batch file failed:',batchfiles)
324+
FAIL('Batch file failed: '+str(batchfiles))
169325

170326
return returncode
171327

@@ -200,6 +356,7 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag
200356
discard_if_ext_present=None,
201357
dispatch=False,
202358
stdOutToFile=False,
359+
preCommands=None,
203360
echo=True):
204361
""" Write one or several batch file, all paths are written relative to the batch file directory.
205362
The batch file will consist of lines of the form:
@@ -255,6 +412,8 @@ def writeb(batchfile, fastfiles):
255412
if not echo:
256413
if os.name == 'nt':
257414
f.write('@echo off\n')
415+
if preCommands is not None:
416+
f.write(preCommands+'\n')
258417
for ff in fastfiles:
259418
ff_abs = os.path.abspath(ff)
260419
ff_rel = os.path.relpath(ff_abs, batchdir)

openfast_toolbox/converters/openfastToHawc2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def FAST2Hawc2(fstIn, htcTemplate, htcOut, OPfile=None, TwrFAFreq=0.1, TwrSSFreq
2525
ED = fst.fst_vt['ElastoDyn']
2626
AD = fst.fst_vt['AeroDyn15']
2727
Bld = fst.fst_vt['AeroDynBlade']
28+
if isinstance(Bld, list):
29+
Bld = Bld[0]
2830
AF = fst.fst_vt['af_data']
2931
twrOF = fst.fst_vt['ElastoDynTower']
3032
BD = fst.fst_vt['BeamDyn']

openfast_toolbox/fastfarm/AMRWindSimulation.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33

44
from openfast_toolbox.fastfarm.FASTFarmCaseCreation import getMultipleOf
5+
from openfast_toolbox.tools.strings import INFO, FAIL, OK, WARN, print_bold
56

67
class AMRWindSimulation:
78
'''
@@ -180,20 +181,20 @@ def _checkInputs(self):
180181
# For convenience, the turbines should not be zero-indexed
181182
if 'name' in self.wts[0]:
182183
if self.wts[0]['name'] != 'T1':
183-
if self.verbose>0: print(f"WARNING: Recommended turbine numbering should start at 1. Currently it is zero-indexed.")
184+
if self.verbose>0: WARN(f"Recommended turbine numbering should start at 1. Currently it is zero-indexed.")
184185

185186

186187
# Flags of given/calculated spatial resolution for warning/error printing purposes
187188
self.given_ds_hr = False
188189
self.given_ds_lr = False
189190
warn_msg = ""
190191
if self.ds_hr is not None:
191-
warn_msg += f"WARNING: HIGH-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON HIGH-RES BOXES CHECKS TO WARNINGS."
192+
warn_msg += f"HIGH-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON HIGH-RES BOXES CHECKS TO WARNINGS."
192193
self.given_ds_hr = True
193194
if self.ds_lr is not None:
194-
warn_msg += f"WARNING: LOW-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON LOW-RES BOX CHECKS TO WARNINGS."
195+
warn_msg += f"LOW-RES SPATIAL RESOLUTION GIVEN. CONVERTING FATAL ERRORS ON LOW-RES BOX CHECKS TO WARNINGS."
195196
self.given_ds_lr = True
196-
if self.verbose>0: print(f'{warn_msg}\n')
197+
if self.verbose>0 and len(warn_msg)>0: WARN(f'{warn_msg}')
197198
a=1
198199

199200

@@ -275,6 +276,8 @@ def _calc_sampling_time(self):
275276
# Calculate dt of high-res per guidelines
276277
dt_hr_max = 1 / (2 * self.fmax_max)
277278
self.dt_high_les = getMultipleOf(dt_hr_max, multipleof=self.dt) # Ensure dt_hr is a multiple of the AMR-Wind timestep
279+
if self.dt_high_les ==0:
280+
raise ValueError(f"AMR-Wind timestep dt={self.dt} is too coarse for high resolution domain! The time step based on `fmax` is {dt_hr_max}, which is too small to be rounded as a multiple of dt.")
278281
else:
279282
# The dt of high-res is given
280283
self.dt_high_les = self.dt_hr
@@ -339,7 +342,7 @@ def _calc_grid_resolution(self):
339342
error_msg = f"AMR-Wind grid spacing of {self.ds_max_at_hr_level} m at the high-res box level of {self.level_hr} is too coarse for "\
340343
f"the high resolution domain. AMR-Wind grid spacing at level {self.level_hr} must be at least {self.ds_high_les} m."
341344
if self.given_ds_hr:
342-
if self.verbose>0: print(f'WARNING: {error_msg}')
345+
if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}')
343346
else:
344347
raise ValueError(error_msg)
345348

@@ -351,7 +354,7 @@ def _calc_grid_resolution(self):
351354
f"to the call to `AMRWindSimulation`. Note that sampled values will no longer be at the cell centers, as you will be requesting "\
352355
f"sampling at {self.ds_low_les} m while the underlying grid will be at {self.ds_max_at_lr_level} m.\n --- SUPRESSING FURTHER ERRORS ---"
353356
if self.given_ds_lr:
354-
if self.verbose>0: print(f'WARNING: {error_msg}')
357+
if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}')
355358
else:
356359
raise ValueError(error_msg)
357360

@@ -382,8 +385,8 @@ def _calc_grid_resolution_lr(self):
382385
# For curled wake model: ds_lr_max = self.cmeander_max * self.dt_low_les * self.vhub**2 / 5
383386

384387
ds_low_les = getMultipleOf(ds_lr_max, multipleof=self.ds_hr)
385-
if self.verbose>0: print(f"Low-res spatial resolution should be at least {ds_lr_max:.2f} m, but since it needs to be a multiple of high-res "\
386-
f"resolution of {self.ds_hr}, we pick ds_low to be {ds_low_les} m")
388+
if self.verbose>0: INFO(f"Low-res spatial resolution (ds_low) should be >={ds_lr_max:.2f} m.\nTo be a multiple of ds_high"\
389+
f"={self.ds_hr} m, we pick ds_low={ds_low_les} m")
387390

388391
#self.ds_lr = self.ds_low_les
389392
return ds_low_les
@@ -636,7 +639,7 @@ def _check_grid_placement_single(self, sampling_xyzgrid_lhr, amr_xyzgrid_at_lhr_
636639
f"AMR-Wind grid (subset): {amr_xyzgrid_at_lhr_level_cc[amr_index ]}, {amr_xyzgrid_at_lhr_level_cc[amr_index+1]}, "\
637640
f"{amr_xyzgrid_at_lhr_level_cc[amr_index+2]}, {amr_xyzgrid_at_lhr_level_cc[amr_index+3]}, ..."
638641
if self.given_ds_lr:
639-
if self.verbose>0: print(f'WARNING: {error_msg}')
642+
if self.verbose>0 and len(error_msg)>0: WARN(f'{error_msg}')
640643
else:
641644
raise ValueError(error_msg)
642645

0 commit comments

Comments
 (0)