diff --git a/docs/howto-make-dlcs.md b/docs/howto-make-dlcs.md index 2c9ee437bf6b89882885f7604b6ab40fae3e67de..d6a92f59b2afa403e8e20636548ad829e2d0ef5f 100644 --- a/docs/howto-make-dlcs.md +++ b/docs/howto-make-dlcs.md @@ -389,27 +389,61 @@ number of cpu's requested (using ```-c``` or ```--nr_cpus```) and minimum of required free cpu's on the cluster (using ```--cpu_free```, 48 by default). Jobs will be launched after a predefined sleep time (as set by the ```--tsleep``` option, and set to 5 seconds by default). After the initial sleep -time a new job will be launched every 0.1 second. If the launch condition is not -met (```nr_cpus > cpu's used by user AND cpu's free on cluster > cpu_free```), -the program will wait 5 seconds before trying to launch a new job again. +time a new job will be launched every 0.5 second. If the launch condition is not +met: + +``` +nr_cpus > cpu's used by user +AND cpu's free on cluster > cpu_free +AND jobs queued by user < cpu_user_queue) +``` + +the program will sleep 5 seconds before trying to launch a new job again. Depending on the amount of jobs and the required computation time, it could take a while before all jobs are launched. When running the launch script from the login node, this might be a problem when you have to close your ssh/putty -session before all jobs are launched. In that case the user should use a -dedicated compute node for launching jobs. To run the launch script on a -compute instead of the login node, use the ```--node``` option. You can inspect -the progress in the ```launch_scheduler_log.txt``` file. +session before all jobs are launched. In that case the user can use the +```--crontab``` argument: it will trigger the ```launch.py``` script every 5 +minutes to check if more jobs can be launched until all jobs have been +executed. The user does not need to have an active ssh/putty session for this to +work. You can follow the progress and configuration of ```launch.py``` in +crontab mode in the following files: + +* ```launch_scheduler_log.txt``` +* ```launch_scheduler_config.txt```: you can change your launch settings on the fly +* ```launch_scheduler_state.txt``` +* ```launch_pbs_filelist.txt```: remaining jobs, when a job is launched it is +removed from this list + +You can check if ```launch.py``` is actually active as a crontab job with: + +``` +crontab -l +``` + +```launch.py``` will clean-up the crontab after all jobs are launched, but if +you need to prevent it from launching new jobs before that, you can clean up your +crontab with: -The ```launch.py``` script has some different options, and you can read about +``` +crontab -r +``` + +The ```launch.py``` script has various different options, and you can read about them by using the help function (the output is included for your convenience): ```bash g-000 $ launch.py --help +Usage: -usage: launch.py -n nr_cpus +launch.py -n nr_cpus -options: +launch.py --crontab when running a single iteration of launch.py as a crontab job every 5 minutes. +File list is read from "launch_pbs_filelist.txt", and the configuration can be changed on the fly +by editing the file "launch_scheduler_config.txt". + +Options: -h, --help show this help message and exit --depend Switch on for launch depend method -n NR_CPUS, --nr_cpus=NR_CPUS @@ -420,15 +454,36 @@ options: full pbs file path. Escape backslashes! By default it will select all *.p files in pbs_in/. --dry dry run: do not alter pbs files, do not launch - --tsleep=TSLEEP Sleep time [s] after qsub command. Default=5 seconds + --tsleep=TSLEEP Sleep time [s] when cluster is too bussy to launch new + jobs. Default=5 seconds + --tsleep_short=TSLEEP_SHORT + Sleep time [s] between between successive job + launches. Default=0.5 seconds. --logfile=LOGFILE Save output to file. -c, --cache If on, files are read from cache --cpu_free=CPU_FREE No more jobs will be launched when the cluster does not have the specified amount of cpus free. This will make sure there is room for others on the cluster, but - might mean less cpus available for you. Default=48. + might mean less cpus available for you. Default=48 + --cpu_user_queue=CPU_USER_QUEUE + No more jobs will be launched after having + cpu_user_queue number of jobs in the queue. This + prevents users from filling the queue, while still + allowing to aim for a high cpu_free target. Default=5 --qsub_cmd=QSUB_CMD Is set automatically by --node flag - --node If executed on dedicated node. + --node If executed on dedicated node. Although this works, + consider using --crontab instead. Default=False + --sort Sort pbs file list. Default=False + --crontab Crontab mode: %prog will check every 5 (default) + minutes if more jobs can be launched. Not compatible + with --node. When all jobs are done, crontab -r will + remove all existing crontab jobs of the current user. + Use crontab -l to inspect current crontab jobs, and + edit them with crontab -e. Default=False + --every_min=EVERY_MIN + Crontab update interval in minutes. Default=5 + --debug Debug print statements. Default=False + ``` Then launch the actual jobs (each job is a ```*.p``` file in ```pbs_in```) using @@ -440,12 +495,17 @@ g-000 $ launch.py -n 100 -p pbs_in/ ``` If the launching process requires hours, and you have to close you SHH/PuTTY -session before it reaches the end, you should use the ```--node``` argument so -the launching process will take place on a dedicated node: +session before it reaches the end, you can either use the ```--node``` or the +```--crontab``` argument. When using ```--node```, ```launch.py``` will run on +a dedicated cluster node, submitted as a PBS job. When using ```--crontab```, +```launch.py``` will be run once every 5 minutes as a ```crontab``` job on the +login node. This is preferred since you are not occupying a node with a very +simple and light job. ```launch.py``` will remove all the users crontab jobs +at the end with ```crontab -r```. ```bash g-000 $ cd /mnt/mimer/hawc2sim/demo/A0001 -g-000 $ launch.py -n 100 -p pbs_in/ --node +g-000 $ launch.py -n 100 -p pbs_in/ --crontab ``` @@ -484,6 +544,19 @@ your running HAWC2 model (replace 123456 with the relevant job id): g-000 $ cd /scratch/$USER/123456.g-000.risoe.dk ``` +You can find what HAWC2 (or whatever other executable you are running) is +outputting to the command line in the file: +``` +/var/lib/torque/spool/JOBID.jess.dtu.dk.OU +``` +Or when watch what is happening at the end in real time +``` +# on Jess: +tail -f /var/lib/torque/spool/JOBID.jess.dtu.dk.OU +# on Gorm: +tail -f /var/spool/pbs/spool/JOBID.g-000.risoe.dk.OU +``` + Re-launching failed jobs ------------------------ @@ -500,11 +573,11 @@ g-000 $ launch.py -n 100 --node -p pbs_in_failed ``` 2. Use the ```--cache``` option, and edit the PBS file list in the file -```pbs_in_file_cache.txt``` so that only the simulations remain that have to be -run again. Note that the ```pbs_in_file_cache.txt``` file is created every time -you run a ```launch.py```. Note that you can use the option ```--dry``` to make -a practice launch run, and that will create a ```pbs_in_file_cache.txt``` file, -but not a single job will be launched. +```launch_pbs_filelist.txt``` so that only the simulations remain that have to be +run again. ```launch_pbs_filelist.txt``` is created every time you run +```launch.py```. You can use the option ```--dry``` to make a practice launch +run, and that will create a ```launch_pbs_filelist.txt``` file, but not a single +job will be launched. 3. Each pbs file can be launched manually as follows: ``` diff --git a/docs/install-anaconda.md b/docs/install-anaconda.md index f22d59b2745060de601de88838310ef74010ac69..0fb52e2149ae397fde6078e1cf91b731ecb25513 100644 --- a/docs/install-anaconda.md +++ b/docs/install-anaconda.md @@ -2,14 +2,25 @@ # Installation manual -## Anaconda or Miniconda +## Anaconda or Miniconda on Linux ``` conda update --all conda create -n wetb_py3 python=3.5 source activate wetb_py3 conda install setuptools_scm future h5py pytables pytest nose sphinx blosc -conda install scipy pandas matplotlib cython xlrd coverage xlwt openpyxl -pip install pyscaffold pytest-cov --no-deps +conda install scipy pandas matplotlib cython xlrd coverage xlwt openpyxl paramiko +conda install -c https://conda.anaconda.org/conda-forge pyscaffold pytest-cov +``` + +## Anaconda or Miniconda on Windows + +``` +conda update --all +conda create -n wetb_py3 python=3.4 +source activate wetb_py3 +conda install setuptools_scm future h5py pytables pytest nose sphinx +conda install scipy pandas matplotlib cython xlrd coverage xlwt openpyxl paramiko +conda install -c https://conda.anaconda.org/conda-forge pyscaffold pytest-cov ``` diff --git a/wetb/control/__init__.py b/wetb/control/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/wetb/control/control.py b/wetb/control/control.py new file mode 100644 index 0000000000000000000000000000000000000000..771c97d80cf285e125c44296c07b87ab3ed6f789 --- /dev/null +++ b/wetb/control/control.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 04 09:24:51 2016 + +@author: tlbl +""" + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +import numpy as np + + +class Control(object): + + def pitch_controller_tuning(self, pitch, I, dQdt, P, Omr, om, csi): + """ + + Function to compute the gains of the pitch controller of the Basic DTU + Wind Energy Controller with the pole placement technique implemented + in HAWCStab2. + + Parameters + ---------- + pitch: array + Pitch angle [deg]. The values should only be those of the full load + region. + I: array + Drivetrain inertia [kg*m**2] + dQdt: array + Partial derivative of the aerodynamic torque with respect to the + pitch angle [kNm/deg]. Can be computed with HAWCStab2. + P: float + Rated power [kW]. Set to zero in case of constant torque regulation + Omr: float + Rated rotational speed [rpm] + om: float + Freqeuncy of regulator mode [Hz] + csi: float + Damping ratio of regulator mode + + Returns + ------- + kp: float + Proportional gain [rad/(rad/s)] + ki: float + Intagral gain [rad/rad] + K1: float + Linear term of the gain scheduling [deg] + K2: float + Quadratic term of the gain shceduling [deg**2] + + + """ + pitch = pitch * np.pi/180. + I = I * 1e-3 + dQdt = dQdt * 180./np.pi + Omr = Omr * np.pi/30. + om = om * 2.*np.pi + + # Quadratic fitting of dQdt + A = np.ones([dQdt.shape[0], 3]) + A[:, 0] = pitch**2 + A[:, 1] = pitch + b = dQdt + ATA = np.dot(A.T, A) + iATA = np.linalg.inv(ATA) + iATAA = np.dot(iATA, A.T) + x = np.dot(iATAA, b) + + kp = -(2*csi*om*I[0] - P/(Omr**2))/x[2] + ki = -(om**2*I[0])/x[2] + + K1 = x[2]/x[1]*(180./np.pi) + K2 = x[2]/x[0]*(180./np.pi)**2 + + return kp, ki, K1, K2 + + def K_omega2(V, P, R, TSR): + + Va = np.array(V) + Pa = np.array(P) + Ra = np.array(R) + TSRa = np.array(TSR) + K = Ra**3 * np.mean(Pa/(TSRa*Va)**3) + + return K + + def select_regions(self, pitch, omega, power): + + i12 = 0 + + n = len(pitch) + + for i in range(n-1): + if (abs(power[i]/power[i+1] - 1.) > 0.01): + if (abs(omega[i] / omega[i+1] - 1.) > 0.01): + i12 = i + break + i23 = n-1 + for i in range(i12, n-1): + if (abs(omega[i] / omega[i+1] - 1.) < 0.01): + i23 = i + break + + i34 = i23 + for i in range(i23, n-1): + if (abs(power[i]/power[i+1] - 1.) > 0.01): + if (abs(omega[i] / omega[i+1] - 1.) < 0.01): + i34 = i+1 + + return i12, i23, i34 + + +if __name__ == '__main__': + + pass diff --git a/wetb/control/tests/test_control.py b/wetb/control/tests/test_control.py new file mode 100644 index 0000000000000000000000000000000000000000..718d740b9c2dbb89bda4693d9a573e90cd873cc7 --- /dev/null +++ b/wetb/control/tests/test_control.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 04 11:09:43 2016 + +@author: tlbl +""" + +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from future import standard_library +standard_library.install_aliases() +import unittest + +from wetb.control import control +import numpy as np + + +class TestControl(unittest.TestCase): + + def setUp(self): + dQdt = np.array([-0.126876918E+03, -0.160463547E+03, -0.211699586E+03, + -0.277209984E+03, -0.351476131E+03, -0.409411354E+03, + -0.427060299E+03, -0.608498644E+03, -0.719141594E+03, + -0.814129630E+03, -0.899248007E+03, -0.981457622E+03, + -0.106667910E+04, -0.114961807E+04, -0.123323210E+04, + -0.131169210E+04, -0.138758236E+04, -0.145930419E+04, + -0.153029102E+04, -0.159737975E+04, -0.166846850E+04]) + + pitch = np.array([0.2510780000E+00, 0.5350000000E-03, 0.535000000E-03, + 0.5350000000E-03, 0.5350000000E-03, 0.535000000E-03, + 0.5350000000E-03, 0.3751976000E+01, 0.625255700E+01, + 0.8195032000E+01, 0.9857780000E+01, 0.113476710E+02, + 0.1271615400E+02, 0.1399768300E+02, 0.152324310E+02, + 0.1642177100E+02, 0.1755302300E+02, 0.186442750E+02, + 0.1970333100E+02, 0.2073358600E+02, 0.217410280E+02]) + + I = np.array([0.4394996114E+08, 0.4395272885E+08, 0.4395488725E+08, + 0.4395301987E+08, 0.4394561932E+08, 0.4393327166E+08, + 0.4391779133E+08, 0.4394706335E+08, 0.4395826989E+08, + 0.4396263773E+08, 0.4396412693E+08, 0.4396397777E+08, + 0.4396275304E+08, 0.4396076315E+08, 0.4395824699E+08, + 0.4395531228E+08, 0.4395201145E+08, 0.4394837798E+08, + 0.4394456127E+08, 0.4394060604E+08, 0.4393647769E+08]) + + self.dQdt = dQdt + self.pitch = pitch + self.I = I + + def test_pitch_controller_tuning(self): + + crt = control.Control() + P = 0. + Omr = 12.1 + om = 0.10 + csi = 0.7 + i = 5 + kp, ki, K1, K2 = crt.pitch_controller_tuning(self.pitch[i:], + self.I[i:], + self.dQdt[i:], + P, Omr, om, csi) + + self.assertAlmostEqual(kp, 1.596090243644432, places=10) + self.assertAlmostEqual(ki, 0.71632362627138424, places=10) + self.assertAlmostEqual(K1, 10.01111637532056, places=10) + self.assertAlmostEqual(K2, 599.53659803157643, places=10) + + def test_regions(self): + + crt = control.Control() + + pitch = np.array([0.,-2.,-2.,-2.,-2.,-2.,-2.,-1., 0., ]) + omega = np.array([1., 1., 1., 2., 3., 3., 3., 3., 3., ]) + power = np.array([1., 2., 3., 4., 5., 6., 7., 7., 7., ]) + + istart, iend = 0, -1 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 2) + self.assertEqual(i2, 4) + self.assertEqual(i3, 6) + + istart, iend = 3, -1 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 1) + self.assertEqual(i3, 3) + + istart, iend = 5, -1 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 0) + self.assertEqual(i3, 1) + + istart, iend = 6, -1 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 0) + self.assertEqual(i3, 0) + + istart, iend = 5, -2 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 0) + self.assertEqual(i3, 1) + + istart, iend = 3, -3 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 1) + self.assertEqual(i3, 2) + + istart, iend = 2, -4 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 2) + self.assertEqual(i3, 2) + + istart, iend = 0, 3 + i1, i2, i3 = crt.select_regions(pitch[istart:iend], omega[istart:iend], + power[istart:iend]) + self.assertEqual(i1, 0) + self.assertEqual(i2, 0) + self.assertEqual(i3, 2) + +if __name__ == "__main__": + + unittest.main() diff --git a/wetb/prepost/Simulations.py b/wetb/prepost/Simulations.py index 4684adbd139e9f7473603cd7409d0a9052e04e8e..89ea46e44eba842512627b41f3a85737de1fbb29 100755 --- a/wetb/prepost/Simulations.py +++ b/wetb/prepost/Simulations.py @@ -6,6 +6,7 @@ Created on Tue Nov 1 15:16:34 2011 __author__ = "David Verelst <dave@dtu.dk>" __license__ = "GPL-2+" """ + from __future__ import print_function from __future__ import division from __future__ import unicode_literals @@ -20,10 +21,6 @@ from future import standard_library standard_library.install_aliases() from builtins import object - - -#print(*objects, sep=' ', end='\n', file=sys.stdout) - # standard python library import os import subprocess as sproc @@ -42,6 +39,7 @@ from operator import itemgetter from time import time #import Queue #import threading +#from multiprocessing import Pool # numpy and scipy only used in HtcMaster._all_in_one_blade_tag import numpy as np @@ -560,7 +558,7 @@ def prepare_launch(iter_dict, opt_tags, master, variable_tag_func, update_cases=False, ignore_non_unique=False, wine_appendix='', run_only_new=False, windows_nr_cpus=2, qsub='', pbs_fname_appendix=True, short_job_names=True, - update_model_data=True): + update_model_data=True, maxcpu=1, pyenv='wetb_py3'): """ Create the htc files, pbs scripts and replace the tags in master file ===================================================================== @@ -749,7 +747,7 @@ def prepare_launch(iter_dict, opt_tags, master, variable_tag_func, # create directory if post_dir does not exists try: - os.mkdir(post_dir) + os.makedirs(post_dir) except OSError: pass FILE = open(fpath_post_base + '.pkl', 'wb') @@ -795,7 +793,8 @@ def prepare_launch(iter_dict, opt_tags, master, variable_tag_func, launch(cases, runmethod=runmethod, verbose=verbose, check_log=check_log, copyback_turb=copyback_turb, qsub=qsub, wine_appendix=wine_appendix, windows_nr_cpus=windows_nr_cpus, short_job_names=short_job_names, - pbs_fname_appendix=pbs_fname_appendix, silent=silent) + pbs_fname_appendix=pbs_fname_appendix, silent=silent, maxcpu=maxcpu, + pyenv=pyenv) return cases @@ -976,7 +975,7 @@ def prepare_launch_cases(cases, runmethod='gorm', verbose=False,write_htc=True, # create directory if post_dir does not exists try: - os.mkdir(post_dir) + os.makedirs(post_dir) except OSError: pass FILE = open(post_dir + master.tags['[sim_id]'] + '.pkl', 'wb') @@ -993,10 +992,10 @@ def prepare_launch_cases(cases, runmethod='gorm', verbose=False,write_htc=True, return cases_new - def launch(cases, runmethod='local', verbose=False, copyback_turb=True, silent=False, check_log=True, windows_nr_cpus=2, qsub='time', - wine_appendix='', pbs_fname_appendix=True, short_job_names=True): + wine_appendix='', pbs_fname_appendix=True, short_job_names=True, + maxcpu=1, pyenv='wetb_py3'): """ The actual launching of all cases in the Cases dictionary. Note that here only the PBS files are written and not the actuall htc files. @@ -1031,10 +1030,11 @@ def launch(cases, runmethod='local', verbose=False, copyback_turb=True, # create the pbs object pbs = PBS(cases, server=runmethod, short_job_names=short_job_names, pbs_fname_appendix=pbs_fname_appendix, qsub=qsub, - verbose=verbose, silent=silent) + verbose=verbose, silent=silent, pyenv=pyenv) pbs.wine_appendix = wine_appendix pbs.copyback_turb = copyback_turb pbs.pbs_out_dir = pbs_out_dir + pbs.maxcpu = maxcpu pbs.create() elif runmethod == 'local': cases = run_local(cases, silent=silent, check_log=check_log) @@ -1047,6 +1047,7 @@ def launch(cases, runmethod='local', verbose=False, copyback_turb=True, 'linux-script, windows-script, local-ram, none' raise ValueError(msg) + def post_launch(cases, save_iter=False, silent=False, suffix=None, path_errorlog=None): """ @@ -1887,7 +1888,8 @@ class PBS(object): """ def __init__(self, cases, server='gorm', qsub='time', silent=False, - pbs_fname_appendix=True, short_job_names=True, verbose=False): + pbs_fname_appendix=True, short_job_names=True, verbose=False, + pyenv='wetb_py3'): """ Define the settings here. This should be done outside, but how? In a text file, paramters list or first create the object and than set @@ -1919,6 +1921,7 @@ class PBS(object): self.server = server self.verbose = verbose self.silent = silent + self.pyenv = pyenv # if server == 'thyra': # self.maxcpu = 4 @@ -1927,11 +1930,12 @@ class PBS(object): self.maxcpu = 1 self.secperiter = 0.012 self.wine = 'time WINEARCH=win32 WINEPREFIX=~/.wine32 wine' + self.winefix = '' elif server == 'jess': self.maxcpu = 1 self.secperiter = 0.012 - self.wine = 'WINEARCH=win32 WINEPREFIX=~/.wine32 winefix\n' - self.wine += 'time WINEARCH=win32 WINEPREFIX=~/.wine32 wine' + self.winefix = 'WINEARCH=win32 WINEPREFIX=~/.wine32 winefix\n' + self.wine = 'time WINEARCH=win32 WINEPREFIX=~/.wine32 wine' else: raise UserWarning('server support only for jess or gorm') @@ -2040,6 +2044,8 @@ class PBS(object): # load all relevant dir settings: the result/logfile/turbulence/zip # they are now also available for starting() and ending() parts hawc2_exe = tag_dict['[hawc2_exe]'] + self.case = case.replace('.htc', '') + self.sim_id = tag_dict['[sim_id]'] self.results_dir = tag_dict['[res_dir]'] self.eigenfreq_dir = tag_dict['[eigenfreq_dir]'] self.logs_dir = tag_dict['[log_dir]'] @@ -2061,6 +2067,7 @@ class PBS(object): self.pbs_queue_command = tag_dict['[pbs_queue_command]'] self.walltime = tag_dict['[walltime]'] self.dyn_walltime = tag_dict['[auto_walltime]'] + self.case_duration = tag_dict['[duration]'] # create the pbs_out_dir if necesary try: @@ -2133,29 +2140,42 @@ class PBS(object): # ----------------------------------------------------------------- # WRITING THE ACTUAL JOB PARAMETERS + # browse to the curren scratch directory + self.pbs += "\n\n" + self.pbs += '# ' + '-'*60 + '\n' + # evaluates to true if LAUNCH_PBS_MODE is NOT set + self.pbs += "# evaluates to true if LAUNCH_PBS_MODE is NOT set\n" + self.pbs += "if [ -z ${LAUNCH_PBS_MODE+x} ] ; then\n" + self.pbs += " echo \n" + self.pbs += " echo 'Execute commands on scratch nodes'\n" + self.pbs += " cd %s/$USER/$PBS_JOBID\n" % self.node_run_root + self.pbs += " # create unique dir for each CPU\n" + self.pbs += ' mkdir "%i"; cd "%i"\n' % (count1, count1) + # output the current scratch directory - self.pbs += "pwd\n" + self.pbs += " pwd\n" # zip file has been copied to the node before (in start_pbs()) - # unzip now in the node - self.pbs += "/usr/bin/unzip " + self.ModelZipFile + '\n' + # unzip now in the CPU scratch directory (zip file is one level up) + self.pbs += " /usr/bin/unzip ../" + self.ModelZipFile + '\n' # create all directories, especially relevant if there are case # dependent sub directories that are not present in the ZIP file - self.pbs += "mkdir -p " + self.htc_dir + '\n' - self.pbs += "mkdir -p " + self.results_dir + '\n' - self.pbs += "mkdir -p " + self.logs_dir + '\n' - self.pbs += "mkdir -p " + self.TurbDirName + '\n' - if self.WakeDirName: - self.pbs += "mkdir -p " + self.WakeDirName + '\n' - if self.MeanderDirName: - self.pbs += "mkdir -p " + self.MeanderDirName + '\n' + self.pbs += " mkdir -p " + self.htc_dir + '\n' + self.pbs += " mkdir -p " + self.results_dir + '\n' + self.pbs += " mkdir -p " + self.logs_dir + '\n' + if self.TurbDirName is not None or self.TurbDirName != 'None': + self.pbs += " mkdir -p " + self.TurbDirName + '\n' + if self.WakeDirName and self.WakeDirName != self.TurbDirName: + self.pbs += " mkdir -p " + self.WakeDirName + '\n' + if self.MeanderDirName and self.MeanderDirName != self.TurbDirName: + self.pbs += " mkdir -p " + self.MeanderDirName + '\n' if self.hydro_dir: - self.pbs += "mkdir -p " + self.hydro_dir + '\n' + self.pbs += " mkdir -p " + self.hydro_dir + '\n' # create the eigen analysis dir just in case that is necessary if self.eigenfreq_dir: - self.pbs += 'mkdir -p %s \n' % self.eigenfreq_dir + self.pbs += ' mkdir -p %s \n' % self.eigenfreq_dir # and copy the htc file to the node - self.pbs += "cp -R $PBS_O_WORKDIR/" + self.htc_dir \ + self.pbs += " cp -R $PBS_O_WORKDIR/" + self.htc_dir \ + case +" ./" + self.htc_dir + '\n' # if there is a turbulence file data base dir, copy from there @@ -2168,16 +2188,16 @@ class PBS(object): # names: turb_base_name_xxx_u.bin, turb_base_name_xxx_v.bin if self.turb_base_name is not None: turb_src = os.path.join(turb_dir_src, self.turb_base_name) - self.pbs += "cp -R %s*.bin %s \n" % (turb_src, self.TurbDirName) + self.pbs += " cp -R %s*.bin %s \n" % (turb_src, self.TurbDirName) # more generally, literally define the names of the boxes for u,v,w # components elif '[turb_fname_u]' in tag_dict: turb_u = os.path.join(turb_dir_src, tag_dict['[turb_fname_u]']) turb_v = os.path.join(turb_dir_src, tag_dict['[turb_fname_v]']) turb_w = os.path.join(turb_dir_src, tag_dict['[turb_fname_w]']) - self.pbs += "cp %s %s \n" % (turb_u, self.TurbDirName) - self.pbs += "cp %s %s \n" % (turb_v, self.TurbDirName) - self.pbs += "cp %s %s \n" % (turb_w, self.TurbDirName) + self.pbs += " cp %s %s \n" % (turb_u, self.TurbDirName) + self.pbs += " cp %s %s \n" % (turb_v, self.TurbDirName) + self.pbs += " cp %s %s \n" % (turb_w, self.TurbDirName) # if there is a turbulence file data base dir, copy from there if self.wakeDb and self.WakeDirName: @@ -2186,7 +2206,7 @@ class PBS(object): wake_dir_src = os.path.join('$PBS_O_WORKDIR', self.WakeDirName) if self.wake_base_name is not None: wake_src = os.path.join(wake_dir_src, self.wake_base_name) - self.pbs += "cp -R %s*.bin %s \n" % (wake_src, self.WakeDirName) + self.pbs += " cp -R %s*.bin %s \n" % (wake_src, self.WakeDirName) # if there is a turbulence file data base dir, copy from there if self.meandDb and self.MeanderDirName: @@ -2195,16 +2215,57 @@ class PBS(object): meand_dir_src = os.path.join('$PBS_O_WORKDIR', self.MeanderDirName) if self.meand_base_name is not None: meand_src = os.path.join(meand_dir_src, self.meand_base_name) - self.pbs += "cp -R %s*.bin %s \n" % (meand_src, self.MeanderDirName) + self.pbs += " cp -R %s*.bin %s \n" % (meand_src, self.MeanderDirName) # copy and rename input files with given versioned name to the # required non unique generic version for fname, fgen in zip(self.copyto_files, self.copyto_generic): - self.pbs += "cp -R $PBS_O_WORKDIR/%s ./%s \n" % (fname, fgen) + self.pbs += " cp -R $PBS_O_WORKDIR/%s ./%s \n" % (fname, fgen) + + # only apply the wine fix in PBS mode + self.pbs += ' ' + self.winefix + # TODO: activate python env, calculate post-processing +# self.pbs += 'echo `python -c "import wetb; print(wetb.__version__)"`\n' + + # end of the file copying in PBS mode + self.pbs += '# ' + '-'*60 + '\n' + self.pbs += "else\n" + # when in find+xargs mode, browse to the relevant CPU + self.pbs += ' # with find+xargs we first browse to CPU folder\n' + self.pbs += ' cd "$CPU_NR"\n' + self.pbs += "fi\n" + self.pbs += '# ' + '-'*60 + '\n' - # the hawc2 execution commands via wine + self.pbs += 'echo ""\n' + self.pbs += '# evaluates to true if LAUNCH_PBS_MODE is NOT set\n' + self.pbs += "if [ -z ${LAUNCH_PBS_MODE+x} ] ; then\n" + # the hawc2 execution commands via wine, in PBS mode fork and wait + param = (self.wine, hawc2_exe, self.htc_dir+case, self.wine_appendix) + self.pbs += ' echo "execute HAWC2, fork to background"\n' + self.pbs += " %s %s ./%s %s &\n" % param + # FIXME: running post-processing will only work when 1 HAWC2 job + # per PBS file, otherwise you have to wait for each job to finish + # first and then run the post-processing for all those cases + if self.maxcpu == 1: + self.pbs += ' wait\n' + if self.pyenv is not None: + self.pbs += ' echo "POST-PROCESSING"\n' + self.pbs += ' source activate %s\n' % self.pyenv + self.pbs += " " + self.checklogs() + self.pbs += " " + self.postprocessing() + self.pbs += ' source deactivate\n' + self.pbs += "else\n" param = (self.wine, hawc2_exe, self.htc_dir+case, self.wine_appendix) - self.pbs += "%s %s ./%s %s &\n" % param + self.pbs += ' echo "execute HAWC2, do not fork and wait"\n' + self.pbs += " %s %s ./%s %s \n" % param + self.pbs += ' echo "POST-PROCESSING"\n' + self.pbs += " " + self.checklogs() + self.pbs += " " + self.postprocessing() + self.pbs += "fi\n" #self.pbs += "wine get_mac_adresses" + '\n' # self.pbs += "cp -R ./*.mac $PBS_O_WORKDIR/." + '\n' @@ -2256,7 +2317,7 @@ class PBS(object): self.pbs += "### Standard Error" + ' \n' self.pbs += "#PBS -e ./" + self.pbs_out_dir + case_id + ".err" + '\n' # self.pbs += "#PBS -e ./pbs_out/" + jobid + ".err" + '\n' - self.pbs += '#PBS -W umask=003\n' + self.pbs += '#PBS -W umask=0003\n' self.pbs += "### Maximum wallclock time format HOURS:MINUTES:SECONDS\n" # self.pbs += "#PBS -l walltime=" + self.walltime + '\n' self.pbs += "#PBS -l walltime=[walltime]\n" @@ -2292,143 +2353,63 @@ class PBS(object): # short walltime queue (shorter than an hour): '#PBS -q xpresq' # or otherwise for longer jobs: '#PBS -q workq' self.pbs += self.pbs_queue_command + '\n' + self.pbs += '\n' + '# ' + '='*78 + '\n' - self.pbs += "### Create scratch directory and copy data to it \n" + # ignore all the file copying when running in xargs mode: + # when varibale LAUNCH_PBS_MODE is set, file copying is ignored + # and will have to be done elsewhere + # we do this so the same launch script can be used either with the node + # scheduler and the PBS system (for example when re-running cases) + # evaluates to true if LAUNCH_PBS_MODE is NOT set + self.pbs += "if [ -z ${LAUNCH_PBS_MODE+x} ] ; then\n" + + self.pbs += " ### Create scratch directory and copy data to it \n" # output the current directory - self.pbs += "cd $PBS_O_WORKDIR" + '\n' - self.pbs += 'echo "current working dir (pwd):"\n' - self.pbs += "pwd \n" + self.pbs += " cd $PBS_O_WORKDIR" + '\n' + self.pbs += ' echo "current working dir (pwd):"\n' + self.pbs += " pwd \n" # The batch system on Gorm allows more than one job per node. # Because of this the scratch directory name includes both the # user name and the job ID, that is /scratch/$USER/$PBS_JOBID # if not scratch, make the dir if self.node_run_root != '/scratch': - self.pbs += 'mkdir -p %s/$USER\n' % self.node_run_root - self.pbs += 'mkdir -p %s/$USER/$PBS_JOBID\n' % self.node_run_root + self.pbs += ' mkdir -p %s/$USER\n' % self.node_run_root + self.pbs += ' mkdir -p %s/$USER/$PBS_JOBID\n' % self.node_run_root # copy the zip files to the scratch dir on the node - self.pbs += "cp -R ./" + self.ModelZipFile + \ + self.pbs += " cp -R ./" + self.ModelZipFile + \ ' %s/$USER/$PBS_JOBID\n' % (self.node_run_root) - - self.pbs += '\n\n' - self.pbs += 'echo ""\n' - self.pbs += 'echo "Execute commands on scratch nodes"\n' - self.pbs += 'cd %s/$USER/$PBS_JOBID\n' % self.node_run_root + self.pbs += "fi\n" + self.pbs += '# ' + '-'*78 + '\n' def ending(self, pbs_path): """ Last part of the pbs script, including command to write script to disc COPY BACK: from node to """ - - self.pbs += "### wait for jobs to finish \n" - self.pbs += "wait\n" - self.pbs += 'echo ""\n' - self.pbs += 'echo "Copy back from scratch directory" \n' - for i in range(1,self.maxcpu+1,1): + self.pbs += "\n\n" + self.pbs += '# ' + "="*78 + "\n" + self.pbs += "### Epilogue\n" + # evaluates to true if LAUNCH_PBS_MODE is NOT set + self.pbs += '# evaluates to true if LAUNCH_PBS_MODE is NOT set\n' + self.pbs += "if [ -z ${LAUNCH_PBS_MODE+x} ] ; then\n" + self.pbs += " ### wait for jobs to finish \n" + self.pbs += " wait\n" + self.pbs += ' echo ""\n' + self.pbs += '# ' + '-'*78 + '\n' + self.pbs += ' echo "Copy back from scratch directory" \n' + for i in range(1, self.maxcpu+1, 1): # navigate to the cpu dir on the node # The batch system on Gorm allows more than one job per node. # Because of this the scratch directory name includes both the # user name and the job ID, that is /scratch/$USER/$PBS_JOBID - # NB! This is different from Thyra! - self.pbs += "cd %s/$USER/$PBS_JOBID\n" % self.node_run_root - - # create the log, res etc dirs in case they do not exist - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.results_dir + "\n" - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.logs_dir + "\n" - if self.animation_dir: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.animation_dir + "\n" - if self.copyback_turb and self.TurbDb: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.TurbDb + "\n" - elif self.copyback_turb: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.TurbDirName + "\n" - if self.copyback_turb and self.wakeDb: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.wakeDb + "\n" - elif self.WakeDirName: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.WakeDirName + "\n" - if self.copyback_turb and self.meandDb: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.meandDb + "\n" - elif self.MeanderDirName: - self.pbs += "mkdir -p $PBS_O_WORKDIR/" + self.MeanderDirName + "\n" - - # and copy the results and log files frome the node to the - # thyra home dir - self.pbs += "cp -R " + self.results_dir + \ - ". $PBS_O_WORKDIR/" + self.results_dir + ".\n" - self.pbs += "cp -R " + self.logs_dir + \ - ". $PBS_O_WORKDIR/" + self.logs_dir + ".\n" - if self.animation_dir: - self.pbs += "cp -R " + self.animation_dir + \ - ". $PBS_O_WORKDIR/" + self.animation_dir + ".\n" - - if self.eigenfreq_dir: - # just in case the eig dir has subdirs for the results, only - # select the base path and cp -r will take care of the rest - p1 = self.eigenfreq_dir.split('/')[0] - self.pbs += "cp -R %s/. $PBS_O_WORKDIR/%s/. \n" % (p1, p1) - # for eigen analysis with floater, modes are in root - eig_dir_sys = '%ssystem/' % self.eigenfreq_dir - self.pbs += 'mkdir -p $PBS_O_WORKDIR/%s \n' % eig_dir_sys - self.pbs += "cp -R mode* $PBS_O_WORKDIR/%s. \n" % eig_dir_sys - - # only copy the turbulence files back if they do not exist - # for all *.bin files on the node - cmd = 'for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/%s$i ]; ' - cmd += 'then echo "$i exists no copyback"; else echo "$i copyback"; ' - cmd += 'cp $i $PBS_O_WORKDIR/%s; fi; done\n' - # copy back turbulence file? - # browse to the node turb dir - self.pbs += '\necho ""\n' - self.pbs += 'echo "COPY BACK TURB IF APPLICABLE"\n' - if self.TurbDirName: - self.pbs += 'cd %s\n' % self.TurbDirName - if self.copyback_turb and self.TurbDb: - tmp = (self.TurbDb, self.TurbDb) - self.pbs += cmd % tmp - elif self.copyback_turb: - tmp = (self.TurbDirName, self.TurbDirName) - self.pbs += cmd % tmp - if self.TurbDirName: - # and back to normal model root - self.pbs += "cd %s/$USER/$PBS_JOBID\n" % self.node_run_root - - if self.WakeDirName: - self.pbs += 'cd %s\n' % self.WakeDirName - if self.copyback_turb and self.wakeDb: - tmp = (self.wakeDb, self.wakeDb) - self.pbs += cmd % tmp - elif self.copyback_turb and self.WakeDirName: - tmp = (self.WakeDirName, self.WakeDirName) - self.pbs += cmd % tmp - if self.WakeDirName: - # and back to normal model root - self.pbs += "cd %s/$USER/$PBS_JOBID\n" % self.node_run_root - - if self.MeanderDirName: - self.pbs += 'cd %s\n' % self.MeanderDirName - if self.copyback_turb and self.meandDb: - tmp = (self.meandDb, self.meandDb) - self.pbs += cmd % tmp - elif self.copyback_turb and self.MeanderDirName: - tmp = (self.MeanderDirName, self.MeanderDirName) - self.pbs += cmd % tmp - if self.MeanderDirName: - # and back to normal model root - self.pbs += "cd %s/$USER/$PBS_JOBID\n" % self.node_run_root - self.pbs += 'echo "END COPY BACK TURB"\n' - self.pbs += 'echo ""\n\n' - - # copy back any other kind of file specified - if len(self.copyback_frename) == 0: - self.copyback_frename = self.copyback_files - for fname, fnew in zip(self.copyback_files, self.copyback_frename): - self.pbs += "cp -R %s $PBS_O_WORKDIR/%s \n" % (fname, fnew) - - # check what is left - self.pbs += 'echo ""\n' - self.pbs += 'echo "following files are on the node (find .):"\n' - self.pbs += 'find .\n' + self.copyback_all_files("pbs_mode", i) + # find+xargs mode only makes sense when maxcpu==1, cpu handling + # for this mode is handled elsewhere + if self.maxcpu == 1: + self.pbs += 'else\n' + self.copyback_all_files("find+xargs", None) # # and delete it all (but that is not allowed) # self.pbs += 'cd ..\n' @@ -2440,6 +2421,9 @@ class PBS(object): # the batch file is still open at this point???? # self.pbs += "rm " + # end of PBS/find+xargs mode switching if/else + self.pbs += 'fi\n' + # base walltime on the longest simulation in the batch nr_time_steps = max(self.nr_time_steps) # TODO: take into acccount the difference between time steps with @@ -2475,6 +2459,158 @@ class PBS(object): # make the string empty again, for memory self.pbs = '' + def copyback_all_files(self, mode, cpu_nr): + """Copy back all the files from either scratch to run_dir (PBS mode), + or from CPU sub-directory back to main directory in find+xargs mode. + """ + if mode=="find+xargs": + foper = "rsync -a --remove-source-files" # move files instead of copy + dst = os.path.join('..', self.sim_id, '') + dst_db = '../' + cd2model = " cd %s\n" % os.path.join(self.node_run_root, '$USER', + '$PBS_JOBID', '$CPU_NR', '') + pbs_mode = False + else: + foper = "cp -R" + dst = "$PBS_O_WORKDIR/" + dst_db = dst + pbs_mode = True + cd2model = " cd %s\n" % os.path.join(self.node_run_root, '$USER', + '$PBS_JOBID', '%i' % cpu_nr, '') + + # navigate to the cpu dir on the node + # The batch system on Gorm/Jess allows more than one job per node. + # Because of this the scratch directory name includes both the + # user name and the job ID, that is /scratch/$USER/$PBS_JOBID/CPU_NR + self.pbs += cd2model + + # create the log, res etc dirs in case they do not exist. Only relevant + # for pbs_mode, they are created in advance in find+xargs + if pbs_mode: + mk = ' mkdir -p' + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.results_dir)) + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.logs_dir)) + if self.animation_dir: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.animation_dir)) + if self.copyback_turb and self.TurbDb: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.TurbDb)) + elif self.copyback_turb: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.TurbDirName)) + if self.copyback_turb and self.wakeDb: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.wakeDb)) + elif self.WakeDirName and self.WakeDirName != self.TurbDirName: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.WakeDirName)) + if self.copyback_turb and self.meandDb: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.meandDb)) + elif self.MeanderDirName and self.MeanderDirName != self.TurbDirName: + self.pbs += "%s %s\n" % (mk, os.path.join(dst, self.MeanderDirName)) + + # and copy the results and log files frome the scratch to dst + res_dst = os.path.join(dst, self.results_dir, ".") + self.pbs += " %s %s. %s\n" % (foper, self.results_dir, res_dst) + log_dst = os.path.join(dst, self.logs_dir, ".") + self.pbs += " %s %s. %s\n" % (foper, self.logs_dir, log_dst) + if self.animation_dir: + ani_dst = os.path.join(dst, self.animation_dir, ".") + self.pbs += " %s %s. %s\n" % (foper, self.animation_dir, ani_dst) + + if self.eigenfreq_dir: + # just in case the eig dir has subdirs for the results, only + # select the base path and cp -r will take care of the rest + p1 = self.eigenfreq_dir.split('/')[0] + p2 = os.path.join(dst, p1, ".") + self.pbs += " cp -R %s/. %s \n" % (p1, p2) + # for eigen analysis with floater, modes are in root + eig_dir_sys = os.path.join(dst, self.eigenfreq_dir, 'system/', '.') + self.pbs += ' mkdir -p %s \n' % eig_dir_sys + self.pbs += " cp -R mode* %s \n" % eig_dir_sys + self.pbs += " %s mode* %s \n" % (foper, eig_dir_sys) + + # only copy the turbulence files back if they do not exist + # for all *.bin files on the node + cmd = ' for i in `ls *.bin`; do if [ -e %s$i ]; ' + cmd += 'then echo "$i exists no copyback"; else echo "$i copyback"; ' + cmd += 'cp $i %s; fi; done\n' + # copy back turbulence file? + # browse to the node turb dir + self.pbs += '\n echo ""\n' + self.pbs += ' echo "COPY BACK TURB IF APPLICABLE"\n' + if self.copyback_turb and self.TurbDb: + self.pbs += ' cd %s\n' % self.TurbDirName + tmp = (os.path.join(dst_db, self.TurbDb, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + elif self.copyback_turb: + self.pbs += ' cd %s\n' % self.TurbDirName + tmp = (os.path.join(dst, self.TurbDirName, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + + if self.copyback_turb and self.wakeDb: + self.pbs += ' cd %s\n' % self.WakeDirName + tmp = (os.path.join(dst_db, self.wakeDb, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + elif self.copyback_turb and self.WakeDirName: + self.pbs += ' cd %s\n' % self.WakeDirName + tmp = (os.path.join(dst, self.WakeDirName, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + + if self.copyback_turb and self.meandDb: + self.pbs += ' cd %s\n' % self.MeanderDirName + tmp = (os.path.join(dst_db, self.meandDb, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + elif self.copyback_turb and self.MeanderDirName: + self.pbs += ' cd %s\n' % self.MeanderDirName + tmp = (os.path.join(dst, self.MeanderDirName, ''),)*2 + self.pbs += cmd % tmp + # and back to normal model root + self.pbs += cd2model + + self.pbs += ' echo "END COPY BACK TURB"\n' + self.pbs += ' echo ""\n\n' + + # copy back any other kind of files, as specified in copyback_files + self.pbs += ' echo "COPYBACK [copyback_files]/[copyback_frename]"\n' + if len(self.copyback_frename) == 0: + self.copyback_frename = self.copyback_files + for fname, fnew in zip(self.copyback_files, self.copyback_frename): + dst_fnew = os.path.join(dst, fnew) + self.pbs += " %s %s %s \n" % (foper, fname, dst_fnew) + self.pbs += ' echo "END COPYBACK"\n' + self.pbs += ' echo ""\n\n' + + if pbs_mode: + # check what is left + self.pbs += ' echo ""\n' + self.pbs += ' echo "following files are on ' + self.pbs += 'node/cpu %i (find .):"\n' % cpu_nr + self.pbs += ' find .\n' + self.pbs += '# ' + '-'*78 + '\n' + + def checklogs(self): + """ + """ + self.pbs += 'python -c "from wetb.prepost import statsdel; ' + rpl = (os.path.join(self.logs_dir, self.case+'.log')) + self.pbs += 'statsdel.logcheck(\'%s\')"\n' % rpl + + def postprocessing(self): + """Run post-processing just after HAWC2 has ran + """ + self.pbs += 'python -c "from wetb.prepost import statsdel; ' + fsrc = os.path.join(self.results_dir, self.case) + rpl = (fsrc, str(self.case_duration), '.csv') + self.pbs += ('statsdel.calc(\'%s\', no_bins=46, m=[3, 4, 6, 8, 10, 12], ' + 'neq=%s, i0=0, i1=None, ftype=\'%s\')"\n' % rpl) + def check_results(self, cases): """ Cross-check if all simulations on the list have returned a simulation. @@ -2512,10 +2648,11 @@ class PBS(object): # length will be zero if there are no failures return cases_fail + # TODO: rewrite the error log analysis to something better. Take different # approach: start from the case and see if the results are present. Than we # also have the tags_dict available when log-checking a certain case -class ErrorLogs(object): +class ErrorLogs(windIO.LogFile): """ Analyse all HAWC2 log files in any given directory ================================================== @@ -2552,106 +2689,21 @@ class ErrorLogs(object): def __init__(self, silent=False, cases=None, resultfile='ErrorLog.csv'): - self.silent = silent - # specify folder which contains the log files + # call init from base class + super(ErrorLogs, self).__init__() + self.PathToLogs = '' self.ResultFile = resultfile - self.cases = cases - - # the total message list log: - self.MsgListLog = [] - # a smaller version, just indication if there are errors: - self.MsgListLog2 = dict() - - # specify which message to look for. The number track's the order. - # this makes it easier to view afterwards in spreadsheet: - # every error will have its own column - - # error messages that appear during initialisation - self.err_init = {} - self.err_init[' *** ERROR *** Error in com'] = len(self.err_init) - self.err_init[' *** ERROR *** in command '] = len(self.err_init) - # *** WARNING *** A comma "," is written within the command line - self.err_init[' *** WARNING *** A comma ",'] = len(self.err_init) - # *** ERROR *** Not correct number of parameters - self.err_init[' *** ERROR *** Not correct '] = len(self.err_init) - # *** INFO *** End of file reached - self.err_init[' *** INFO *** End of file r'] = len(self.err_init) - # *** ERROR *** No line termination in command line - self.err_init[' *** ERROR *** No line term'] = len(self.err_init) - # *** ERROR *** MATRIX IS NOT DEFINITE - self.err_init[' *** ERROR *** MATRIX IS NO'] = len(self.err_init) - # *** ERROR *** There are unused relative - self.err_init[' *** ERROR *** There are un'] = len(self.err_init) - # *** ERROR *** Error finding body based - self.err_init[' *** ERROR *** Error findin'] = len(self.err_init) - # *** ERROR *** In body actions - self.err_init[' *** ERROR *** In body acti'] = len(self.err_init) - # *** ERROR *** Command unknown and ignored - self.err_init[' *** ERROR *** Command unkn'] = len(self.err_init) - # *** ERROR *** ERROR - More bodies than elements on main_body: tower - self.err_init[' *** ERROR *** ERROR - More'] = len(self.err_init) - # *** ERROR *** The program will stop - self.err_init[' *** ERROR *** The program '] = len(self.err_init) - # *** ERROR *** Unknown begin command in topologi. - self.err_init[' *** ERROR *** Unknown begi'] = len(self.err_init) - # *** ERROR *** Not all needed topologi main body commands present - self.err_init[' *** ERROR *** Not all need'] = len(self.err_init) - # *** ERROR *** opening timoschenko data file - self.err_init[' *** ERROR *** opening tim'] = len(self.err_init) - # *** ERROR *** Error opening AE data file - self.err_init[' *** ERROR *** Error openin'] = len(self.err_init) - # *** ERROR *** Requested blade _ae set number not found in _ae file - self.err_init[' *** ERROR *** Requested bl'] = len(self.err_init) - # Error opening PC data file - self.err_init[' Error opening PC data file'] = len(self.err_init) - # *** ERROR *** error reading mann turbulence - self.err_init[' *** ERROR *** error readin'] = len(self.err_init) - # *** INFO *** The DLL subroutine - self.err_init[' *** INFO *** The DLL subro'] = len(self.err_init) - # ** WARNING: FROM ESYS ELASTICBAR: No keyword - self.err_init[' ** WARNING: FROM ESYS ELAS'] = len(self.err_init) - # *** ERROR *** DLL ./control/killtrans.dll could not be loaded - error! - self.err_init[' *** ERROR *** DLL'] = len(self.err_init) - # *** ERROR *** The DLL subroutine - self.err_init[' *** ERROR *** The DLL subr'] = len(self.err_init) - # *** ERROR *** Mann turbulence length scale must be larger than zero! - # *** ERROR *** Mann turbulence alpha eps value must be larger than zero! - # *** ERROR *** Mann turbulence gamma value must be larger than zero! - self.err_init[' *** ERROR *** Mann turbule'] = len(self.err_init) - - # *** WARNING *** Shear center x location not in elastic center, set to zero - self.err_init[' *** WARNING *** Shear cent'] = len(self.err_init) - # Turbulence file ./xyz.bin does not exist - self.err_init[' Turbulence file '] = len(self.err_init) - self.err_init[' *** WARNING ***'] = len(self.err_init) - self.err_init[' *** ERROR ***'] = len(self.err_init) - self.err_init[' WARNING'] = len(self.err_init) - self.err_init[' ERROR'] = len(self.err_init) - - # error messages that appear during simulation - self.err_sim = {} - # *** ERROR *** Wind speed requested inside - self.err_sim[' *** ERROR *** Wind speed r'] = len(self.err_sim) - # Maximum iterations exceeded at time step: - self.err_sim[' Maximum iterations exceede'] = len(self.err_sim) - # Solver seems not to converge: - self.err_sim[' Solver seems not to conver'] = len(self.err_sim) - # *** ERROR *** Out of x bounds: - self.err_sim[' *** ERROR *** Out of x bou'] = len(self.err_sim) - # *** ERROR *** Out of limits in user defined shear field - limit value used - self.err_sim[' *** ERROR *** Out of limit'] = len(self.err_sim) - - # TODO: error message from a non existing channel output/input - # add more messages if required... - - self.init_cols = len(self.err_init) - self.sim_cols = len(self.err_sim) + self.silent = silent # TODO: save this not a csv text string but a df_dict, and save as excel # and DataFrame! def check(self, appendlog=False, save_iter=False): + """Check all log files that are to be found in the directory + ErrorLogs.PathToLogs, or check the specific log file if + ErrorLogs.PathToLogs points to a specific log file. + """ # MsgListLog = [] @@ -2686,208 +2738,12 @@ class ErrorLogs(object): # open the current log file f_log = os.path.join(self.PathToLogs, str(fname_lower)) - with open(f_log, 'r') as f: - lines = f.readlines() - - # keep track of the messages allready found in this file - tempLog = [] - tempLog.append(fname) - exit_correct, found_error = False, False - - subcols_sim = 4 - subcols_init = 2 - # create empty list item for the different messages and line - # number. Include one column for non identified messages - for j in range(self.init_cols): - # 2 sub-columns per message: nr, msg - for k in range(subcols_init): - tempLog.append('') - for j in range(self.sim_cols): - # 4 sub-columns per message: first, last, nr, msg - for k in range(subcols_sim): - tempLog.append('') - # and two more columns at the end for messages of unknown origin - tempLog.append('') - tempLog.append('') - - # if there is a cases object, see how many time steps we expect - if self.cases is not None: - case = self.cases[fname.replace('.log', '.htc')] - dt = float(case['[dt_sim]']) - time_steps = int(float(case['[time_stop]']) / dt) - iterations = np.ndarray( (time_steps+1,3), dtype=np.float32 ) - else: - iterations = np.ndarray( (len(lines),3), dtype=np.float32 ) - dt = False - iterations[:,0:2] = -1 - iterations[:,2] = 0 - - # keep track of the time_step number - time_step, init_block = -1, True - # check for messages in the current line - # for speed: delete from message watch list if message is found - for j, line in enumerate(lines): - # all id's of errors are 27 characters long - msg = line[:27] - # remove the line terminator, this seems to take 2 characters - # on PY2, but only one in PY3 - line = line.replace('\n', '') - - # keep track of the number of iterations - if line[:12] == ' Global time': - time_step += 1 - iterations[time_step,0] = float(line[14:40]) - # for PY2, new line is 2 characters, for PY3 it is one char - iterations[time_step,1] = int(line[-6:]) - # time step is the first time stamp - if not dt: - dt = float(line[15:40]) - # no need to look for messages if global time is mentioned - continue - elif line[:20] == ' Starting simulation': - init_block = False - - elif init_block: - # if string is shorter, we just get a shorter string. - # checking presence in dict is faster compared to checking - # the length of the string - # first, last, nr, msg - if msg in self.err_init: - # icol=0 -> fname - icol = subcols_init*self.err_init[msg] + 1 - # 0: number of occurances - if tempLog[icol] == '': - tempLog[icol] = '1' - else: - tempLog[icol] = str(int(tempLog[icol]) + 1) - # 1: the error message itself - tempLog[icol+1] = line - found_error = True - - # find errors that can occur during simulation - elif msg in self.err_sim: - icol = subcols_sim*self.err_sim[msg] - icol += subcols_init*self.init_cols + 1 - # in case stuff already goes wrong on the first time step - if time_step == -1: - time_step = 0 - - # 1: time step of first occurance - if tempLog[icol] == '': - tempLog[icol] = '%i' % time_step - # 2: time step of last occurance - tempLog[icol+1] = '%i' % time_step - # 3: number of occurances - if tempLog[icol+2] == '': - tempLog[icol+2] = '1' - else: - tempLog[icol+2] = str(int(tempLog[icol+2]) + 1) - # 4: the error message itself - tempLog[icol+3] = line - - found_error = True - iterations[time_step,2] = 1 - - # method of last resort, we have no idea what message - elif line[:10] == ' *** ERROR' or line[:10]==' ** WARNING': - icol = subcols_sim*self.sim_cols - icol += subcols_init*self.init_cols + 1 - # line number of the message - tempLog[icol] = j - # and message - tempLog[icol+1] = line - found_error = True - # in case stuff already goes wrong on the first time step - if time_step == -1: - time_step = 0 - iterations[time_step,2] = 1 - - # simulation and simulation output time if self.cases is not None: - t_stop = float(case['[time_stop]']) - duration = float(case['[duration]']) - else: - t_stop = -1 - duration = -1 - - # see if the last line holds the sim time - if line[:15] == ' Elapsed time :': - exit_correct = True - elapsed_time = float(line[15:-1]) - tempLog.append( elapsed_time ) - # in some cases, Elapsed time is not given, and the last message - # might be: " Closing of external type2 DLL" - elif line[:20] == ' Closing of external': - exit_correct = True - elapsed_time = iterations[time_step,0] - tempLog.append( elapsed_time ) - elif np.allclose(iterations[time_step,0], t_stop): - exit_correct = True - elapsed_time = iterations[time_step,0] - tempLog.append( elapsed_time ) - else: - elapsed_time = -1 - tempLog.append('') - - # give the last recorded time step - tempLog.append('%1.11f' % iterations[time_step,0]) - - # simulation and simulation output time - tempLog.append('%1.01f' % t_stop) - tempLog.append('%1.04f' % (t_stop/elapsed_time)) - tempLog.append('%1.01f' % duration) - - # as last element, add the total number of iterations - itertotal = np.nansum(iterations[:,1]) - tempLog.append('%i' % itertotal) - - # the delta t used for the simulation - if dt: - tempLog.append('%1.7f' % dt) + case = self.cases[fname.replace('.log', '.htc')] else: - tempLog.append('failed to find dt') - - # number of time steps - tempLog.append('%i' % len(iterations) ) - - # if the simulation didn't end correctly, the elapsed_time doesn't - # exist. Add the average and maximum nr of iterations per step - # or, if only the structural and eigen analysis is done, we have 0 - try: - ratio = float(elapsed_time)/float(itertotal) - tempLog.append('%1.6f' % ratio) - except (UnboundLocalError, ZeroDivisionError, ValueError) as e: - tempLog.append('') - # when there are no time steps (structural analysis only) - try: - tempLog.append('%1.2f' % iterations[:,1].mean() ) - tempLog.append('%1.2f' % iterations[:,1].max() ) - except ValueError: - tempLog.append('') - tempLog.append('') - - # save the iterations in the results folder - if save_iter: - fiter = fname.replace('.log', '.iter') - fmt = ['%12.06f', '%4i', '%4i'] - if self.cases is not None: - fpath = os.path.join(case['[run_dir]'], case['[iter_dir]']) - # in case it has subdirectories - for tt in [3,2,1]: - tmp = os.path.sep.join(fpath.split(os.path.sep)[:-tt]) - if not os.path.exists(tmp): - os.makedirs(tmp) - if not os.path.exists(fpath): - os.makedirs(fpath) - np.savetxt(fpath + fiter, iterations, fmt=fmt) - else: - np.savetxt(os.path.join(self.PathToLogs, fiter), iterations, - fmt=fmt) - - # append the messages found in the current file to the overview log - self.MsgListLog.append(tempLog) - self.MsgListLog2[fname] = [found_error, exit_correct] + case = None + self.readlog(f_log, case=case, save_iter=save_iter) i += 1 # # if no messages are found for the current file, than say so: @@ -2907,21 +2763,8 @@ class ErrorLogs(object): def save(self, appendlog=False, suffix=None): - # write the results in a file, start with a header - contents = 'file name;' + 'nr;msg;'*(self.init_cols) - contents += 'first_tstep;last_tstep;nr;msg;'*(self.sim_cols) - contents += 'lnr;msg;' - # and add headers for elapsed time, nr of iterations, and sec/iteration - contents += 'Elapsted time;last time step;Simulation time;' - contents += 'real sim time;Sim output time;' - contents += 'total iterations;dt;nr time steps;' - contents += 'seconds/iteration;average iterations/time step;' - contents += 'maximum iterations/time step;\n' - for k in self.MsgListLog: - for n in k: - contents = contents + str(n) + ';' - # at the end of each line, new line symbol - contents = contents + '\n' + contents = self._header() + contents = self._msglistlog2csv(contents) # write csv file to disk, append to facilitate more logfile analysis if isinstance(suffix, str): @@ -4082,7 +3925,7 @@ class Cases(object): tags=['[turb_seed]','[windspeed]'], calc_mech_power=False, save=True, m=[3, 4, 6, 8, 10, 12], neq=None, no_bins=46, ch_fatigue={}, update=False, add_sensor=None, - chs_resultant=[], i0=0, i1=-1, saveinterval=1000, + chs_resultant=[], i0=0, i1=None, saveinterval=1000, csv=True, suffix=None, A=None, ch_wind=None, save_new_sigs=False, xlsx=False): """ @@ -4219,7 +4062,7 @@ class Cases(object): if not silent: pc = '%6.2f' % (float(ii)*100.0/float(nrcases)) pc += ' %' - print('stats progress: %4i/%i %s' % (ii, nrcases, pc)) + print('stats progress: %4i/%i %s | %s' % (ii, nrcases, pc, cname)) # make sure the selected tags exist if len(tags) != len(set(case) and tags): @@ -5143,7 +4986,7 @@ class Cases(object): raise ValueError('name should be either ojf or hawc2') # create the torque_constant dir if it doesn't exists try: - os.mkdir(fpath) + os.makedirs(fpath) except OSError: pass @@ -5284,7 +5127,7 @@ class Cases(object): print('='*79) print('statistics for %s, nr cases: %i' % (sim_id, nrcases)) - fname = os.path.join(post_dir, sim_id, '_envelope' + append + '.h5') + fname = os.path.join(post_dir, sim_id + '_envelope' + append + '.h5') h5f = tbl.openFile(fname, mode="w", title=str(sim_id), filters=tbl.Filters(complevel=9)) @@ -5435,7 +5278,7 @@ class MannTurb64(prepost.PBSScript): super(MannTurb64, self).__init__() self.exe = 'time wine mann_turb_x64.exe' # PBS configuration - self.umask = '003' + self.umask = '0003' self.walltime = '00:59:59' self.queue = 'workq' self.lnodes = '1' @@ -5473,9 +5316,9 @@ class MannTurb64(prepost.PBSScript): self.prelude = 'cd %s' % case['[turb_dir]'] # alfaeps, L, gamma, seed, nr_u, nr_v, nr_w, du, dv, dw high_freq_comp - rpl = (float(case['[AlfaEpsilon]']), - float(case['[L_mann]']), - float(case['[Gamma]']), + rpl = (float(case['[MannAlfaEpsilon]']), + float(case['[MannL]']), + float(case['[MannGamma]']), int(case['[tu_seed]']), int(case['[turb_nr_u]']), int(case['[turb_nr_v]']), @@ -5576,4 +5419,4 @@ def eigenstructure(cases, debug=False): return cases if __name__ == '__main__': - pass \ No newline at end of file + pass diff --git a/wetb/prepost/dlctemplate.py b/wetb/prepost/dlctemplate.py index 95e0a5e9c6f5489c2fa1f56ae70ec1ff6ffc7396..5331c89179942da4bee4e4cb6dea08f4b0350a38 100755 --- a/wetb/prepost/dlctemplate.py +++ b/wetb/prepost/dlctemplate.py @@ -26,6 +26,7 @@ from matplotlib import pyplot as plt from wetb.prepost import Simulations as sim from wetb.prepost import dlcdefs from wetb.prepost import dlcplots +from wetb.prepost.simchunks import create_chunks_htc_pbs plt.rc('font', family='serif') plt.rc('xtick', labelsize=10) @@ -117,9 +118,9 @@ def master_tags(sim_id, runmethod='local', silent=False, verbose=False): # master file with the HAWC2Wrapper !! # default tags turbulence generator (required for 64-bit Mann generator) # alfaeps, L, gamma, seed, nr_u, nr_v, nr_w, du, dv, dw high_freq_comp - master.tags['[AlfaEpsilon]'] = 1.0 - master.tags['[L_mann]'] = 29.4 - master.tags['[Gamma]'] = 3.0 + master.tags['[MannAlfaEpsilon]'] = 1.0 + master.tags['[MannL]'] = 29.4 + master.tags['[MannGamma]'] = 3.0 master.tags['[tu_seed]'] = 0 master.tags['[turb_nr_u]'] = 8192 master.tags['[turb_nr_v]'] = 32 @@ -182,8 +183,8 @@ def variable_tag_func(master, case_id_short=False): ### PRE- POST # ============================================================================= -def launch_dlcs_excel(sim_id, silent=False, verbose=False, pbs_turb=True, - runmethod=None, write_htc=True): +def launch_dlcs_excel(sim_id, silent=False, verbose=False, pbs_turb=False, + runmethod=None, write_htc=True, zipchunks=False): """ Launch load cases defined in Excel files """ @@ -240,7 +241,7 @@ def launch_dlcs_excel(sim_id, silent=False, verbose=False, pbs_turb=True, copyback_turb=True, update_cases=False, msg='', ignore_non_unique=False, run_only_new=False, pbs_fname_appendix=False, short_job_names=False, - silent=silent, verbose=verbose) + silent=silent, verbose=verbose, pyenv=None) if pbs_turb: # to avoid confusing HAWC2 simulations and Mann64 generator PBS files, @@ -248,56 +249,24 @@ def launch_dlcs_excel(sim_id, silent=False, verbose=False, pbs_turb=True, mann64 = sim.MannTurb64(silent=silent) mann64.gen_pbs(cases) - -def launch_param(sim_id): - """ - Launch parameter variations defined according to the Simulations syntax - """ - # MODEL SOURCES, exchanche file sources -# p_local = '/mnt/vea-group/AED/STABCON/SIM/NREL5MW' -# p_local = '%s/%s' % (P_SOURCE, PROJECT) - # target run dir (is defined in the master_tags) -# p_root = '/mnt/gorm/HAWC2/NREL5MW' - - iter_dict = dict() - iter_dict['[Windspeed]'] = [False] - - opt_tags = [] - - runmethod = 'gorm' -# runmethod = 'local' -# runmethod = 'linux-script' -# runmethod = 'windows-script' -# runmethod = 'jess' - master = master_tags(sim_id, runmethod=runmethod) - master.tags['[hawc2_exe]'] = 'hawc2-latest' - master.tags['[sim_id]'] = sim_id - master.output_dirs.append('[Case folder]') - master.output_dirs.append('[Case id.]') - - # TODO: copy master and DLC exchange files to p_root too!! - - # all tags set in master_tags will be overwritten by the values set in - # variable_tag_func(), iter_dict and opt_tags - # values set in iter_dict have precedence over opt_tags - # variable_tag_func() has precedense over iter_dict, which has precedence - # over opt_tags. So opt_tags comes last - sim.prepare_launch(iter_dict, opt_tags, master, variable_tag_func, - write_htc=True, runmethod=runmethod, verbose=False, - copyback_turb=False, msg='', update_cases=False, - ignore_non_unique=False, run_only_new=False, - pbs_fname_appendix=False, short_job_names=False) + if zipchunks: + # create chunks + # sort so we have minimal copying turb files from mimer to node/scratch + sorts_on = ['[DLC]', '[Windspeed]'] + create_chunks_htc_pbs(cases, sort_by_values=sorts_on, ppn=20, + nr_procs_series=9, processes=1, + walltime='20:00:00', chunks_dir='zip-chunks-jess') + create_chunks_htc_pbs(cases, sort_by_values=sorts_on, ppn=12, + nr_procs_series=15, processes=1, + walltime='20:00:00', chunks_dir='zip-chunks-gorm') def post_launch(sim_id, statistics=True, rem_failed=True, check_logs=True, force_dir=False, update=False, saveinterval=2000, csv=False, m=[1, 3, 4, 5, 6, 8, 10, 12, 14], neq=None, no_bins=46, - years=20.0, fatigue=True, nn_twb=1, nn_twt=20, nn_blr=4, A=None, + years=20.0, fatigue=True, A=None, AEP=False, save_new_sigs=False, envelopeturbine=False, envelopeblade=False, - save_iter=False, AEP=False): - - if neq < 0: - neq = None + save_iter=False): # ========================================================================= # check logfiles, results files, pbs output files @@ -331,36 +300,6 @@ def post_launch(sim_id, statistics=True, rem_failed=True, check_logs=True, df_stats, df_AEP, df_Leq = None, None, None if statistics: - # for the default load case analysis, add mechanical power -# add = {'ch1_name':'shaft-shaft-node-004-momentvec-z', -# 'ch2_name':'Omega', -# 'ch_name_add':'mechanical-power-floater-floater-001', -# 'factor':1.0, 'operator':'*'} - # for the AVATAR DLB, following resultants are defined: - chs_resultant = [['tower-tower-node-%03i-momentvec-x' % nn_twb, - 'tower-tower-node-%03i-momentvec-y' % nn_twb], - ['tower-tower-node-%03i-momentvec-x' % nn_twt, - 'tower-tower-node-%03i-momentvec-y' % nn_twt], - ['shaft-shaft-node-004-momentvec-x', - 'shaft-shaft-node-004-momentvec-z'], - ['shaft-shaft-node-004-momentvec-y', - 'shaft-shaft-node-004-momentvec-z'], - ['shaft_nonrotate-shaft-node-004-momentvec-x', - 'shaft_nonrotate-shaft-node-004-momentvec-z'], - ['shaft_nonrotate-shaft-node-004-momentvec-y', - 'shaft_nonrotate-shaft-node-004-momentvec-z'], - ['blade1-blade1-node-%03i-momentvec-x' % nn_blr, - 'blade1-blade1-node-%03i-momentvec-y' % nn_blr], - ['blade2-blade2-node-%03i-momentvec-x' % nn_blr, - 'blade2-blade2-node-%03i-momentvec-y' % nn_blr], - ['blade3-blade3-node-%03i-momentvec-x' % nn_blr, - 'blade3-blade3-node-%03i-momentvec-y' % nn_blr], - ['hub1-blade1-node-%03i-momentvec-x' % nn_blr, - 'hub1-blade1-node-%03i-momentvec-y' % nn_blr], - ['hub2-blade2-node-%03i-momentvec-x' % nn_blr, - 'hub2-blade2-node-%03i-momentvec-y' % nn_blr], - ['hub3-blade3-node-%03i-momentvec-x' % nn_blr, - 'hub3-blade3-node-%03i-momentvec-y' % nn_blr]] i0, i1 = 0, -1 # in addition, sim_id and case_id are always added by default @@ -372,7 +311,7 @@ def post_launch(sim_id, statistics=True, rem_failed=True, check_logs=True, update=update, saveinterval=saveinterval, suffix=suffix, save_new_sigs=save_new_sigs, csv=csv, m=m, neq=neq, no_bins=no_bins, - chs_resultant=chs_resultant, A=A) + chs_resultant=[], A=A) # annual energy production if AEP: df_AEP = cc.AEP(df_stats, csv=csv, update=update, save=True) @@ -436,15 +375,11 @@ if __name__ == '__main__': dest='years', help='Total life time in years') parser.add_argument('--no_bins', type=float, default=46.0, action='store', dest='no_bins', help='Number of bins for fatigue loads') - parser.add_argument('--neq', type=float, default=-1.0, action='store', + parser.add_argument('--neq', type=float, default=None, action='store', dest='neq', help='Equivalent cycles neq, default 1 Hz ' 'equivalent load (neq = simulation ' 'duration in seconds)') - parser.add_argument('--nn_twt', type=float, default=20, action='store', - dest='nn_twt', help='Node number tower top') - parser.add_argument('--nn_blr', type=float, default=4, action='store', - dest='nn_blr', help='Node number blade root') - parser.add_argument('--rotarea', type=float, default=4, action='store', + parser.add_argument('--rotarea', type=float, default=None, action='store', dest='rotarea', help='Rotor area for C_T, C_P') parser.add_argument('--save_new_sigs', default=False, action='store_true', dest='save_new_sigs', help='Save post-processed sigs') @@ -454,6 +389,16 @@ if __name__ == '__main__': dest='envelopeblade', help='Compute envelopeblade') parser.add_argument('--envelopeturbine', default=False, action='store_true', dest='envelopeturbine', help='Compute envelopeturbine') + parser.add_argument('--zipchunks', default=False, action='store_true', + dest='zipchunks', help='Create PBS launch files for' + 'running in zip-chunk find+xargs mode.') + parser.add_argument('--pbs_turb', default=False, action='store_true', + dest='pbs_turb', help='Create PBS launch files to ' + 'create the turbulence boxes in stand alone mode ' + 'using the 64-bit Mann turbulence box generator. ' + 'This can be usefull if your turbulence boxes are too ' + 'big for running in HAWC2 32-bit mode. Only works on ' + 'Jess. ') opt = parser.parse_args() # TODO: use arguments to determine the scenario: @@ -491,7 +436,8 @@ if __name__ == '__main__': # create HTC files and PBS launch scripts (*.p) if opt.prep: print('Start creating all the htc files and pbs_in files...') - launch_dlcs_excel(sim_id, silent=False) + launch_dlcs_excel(sim_id, silent=False, zipchunks=opt.zipchunks, + pbs_turb=opt.pbs_turb) # post processing: check log files, calculate statistics if opt.check_logs or opt.stats or opt.fatigue or opt.envelopeblade or opt.envelopeturbine: post_launch(sim_id, check_logs=opt.check_logs, update=False, diff --git a/wetb/prepost/misc.py b/wetb/prepost/misc.py index 1d3b3aff9e6500720d06b3cc075db32f82c9b3bc..f25b97897d3060b4591d7160c9c4590cc6aba107 100644 --- a/wetb/prepost/misc.py +++ b/wetb/prepost/misc.py @@ -701,7 +701,8 @@ def read_excel_files(proot, fext='xlsx', pignore=None, sheet=0, that have file extension "fext" fext : string, default='xlsx' - File extension of the Excel files that should be loaded + File extension of the Excel files that should be loaded. Other valid + extensions are csv, xls, and xlsm. pignore : string, default=None Specify which string can not occur in the full path of the DLC target. @@ -711,7 +712,7 @@ def read_excel_files(proot, fext='xlsx', pignore=None, sheet=0, sheet : string or int, default=0 Name or index of the Excel sheet to be considered. By default, the - first sheet (index=0) is taken. + first sheet (index=0) is taken. Ignored when fext is csv. Returns ------- @@ -738,8 +739,10 @@ def read_excel_files(proot, fext='xlsx', pignore=None, sheet=0, if not silent: print(f_target, end='') try: - xl = pd.ExcelFile(f_target) - df = xl.parse(sheet) + if fext == 'csv': + df = pd.read_csv(f_target) + else: + df = pd.read_excel(f_target, sheetname=sheet) df_list[f_target.replace('.'+fext, '')] = df if not silent: print(': sucesfully included %i case(s)' % len(df)) diff --git a/wetb/prepost/simchunks.py b/wetb/prepost/simchunks.py new file mode 100644 index 0000000000000000000000000000000000000000..931cde2d4bef86131c85b23c6826686b0942726e --- /dev/null +++ b/wetb/prepost/simchunks.py @@ -0,0 +1,664 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 8 10:22:49 2016 + +@author: dave +""" +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals +from __future__ import absolute_import +from builtins import dict +from io import open +from builtins import zip +from builtins import range +from builtins import str +from builtins import int +from future import standard_library +standard_library.install_aliases() +from builtins import object + +# standard python library +import os +import zipfile +import copy +import tarfile +import glob + +import numpy as np +import pandas as pd +#from tqdm import tqdm + +from wetb.prepost.Simulations import Cases + + +def create_chunks_htc_pbs(cases, sort_by_values=['[Windspeed]'], ppn=20, + nr_procs_series=9, processes=1, queue='workq', + walltime='24:00:00', chunks_dir='zip-chunks-jess', + pyenv='wetb_py3'): + """Group a large number of simulations htc and pbs launch scripts into + different zip files so we can run them with find+xargs on various nodes. + """ + + def chunker(seq, size): + # for DataFrames you can also use groupby, as taken from: + # http://stackoverflow.com/a/25703030/3156685 + # for k,g in df.groupby(np.arange(len(df))//10) + # but this approach is faster, see also: + # http://stackoverflow.com/a/25701576/3156685 + # http://stackoverflow.com/a/434328/3156685 + return (seq[pos:pos + size] for pos in range(0, len(seq), size)) + + def make_zip_chunks(df, ii, sim_id, run_dir, model_zip): + """Create zip cunks and also create an index + """ + + # create a new zip file, give index of the first element. THis is + # quasi random due to the sorting we applied earlier +# ii = df.index[0] + rpl = (sim_id, ii) + fname = os.path.join(run_dir, chunks_dir, '%s_chnk_%05i' % rpl) + zf = zipfile.ZipFile(fname+'.zip', 'w', compression=zipfile.ZIP_STORED) + + # start with appending the base model zip file + fname_model = os.path.join(run_dir, model_zip) + with zipfile.ZipFile(fname_model, 'r') as zf_model: + for n in zf_model.namelist(): + zf.writestr(n, zf_model.open(n).read()) + + # create all necessary directories in the zip file + dirtags = ['[htc_dir]', '[res_dir]','[log_dir]','[animation_dir]', + '[pbs_in_dir]', '[eigenfreq_dir]','[turb_dir]','[wake_dir]', + '[meander_dir]','[hydro_dir]', '[mooring_dir]', + '[pbs_in_dir]', '[pbs_out_dir]'] + dirnames = [] + for tag in dirtags: + for dirname in set(df[tag].unique().tolist()): + if not dirname or dirname.lower() not in ['false', 'none', 0]: + dirnames.append(dirname) + for dirname in set(dirnames): + if dirname != 0: + zf.write('.', os.path.join(dirname, '.')) + + # and the post-processing data + # FIXME: do not use hard coded paths! + zf.write('.', 'prepost-data/') + + # HTC files + df_src = df['[run_dir]'] + df['[htc_dir]'] + df['[case_id]'] + df_dst = df['[htc_dir]'] + df['[case_id]'] + # create an index so given the htc file, we can find the chunk nr + df_index = pd.DataFrame(index=df['[case_id]'].copy(), + columns=['chnk_nr', 'name']) + df_index['chnk_nr'] = ii + df_index['name'] = os.path.join(chunks_dir, '%s_chnk_%05i' % rpl) + # Since df_src and df_dst are already Series, iterating is fast an it + # is slower to first convert to a list + for src, dst_rel in zip(df_src, df_dst): + zf.write(src+'.htc', dst_rel+'.htc') + + # PBS files + df_src = df['[run_dir]'] + df['[pbs_in_dir]'] + df['[case_id]'] + df_dst = df['[pbs_in_dir]'] + df['[case_id]'] + # Since df_src and df_dst are already Series, iterating is fast an it + # is slower to first convert to a list + for src, dst_rel in zip(df_src, df_dst): + zf.write(src+'.p', dst_rel+'.p') + + # copy and rename input files with given versioned name to the + # all files that will have to be renamed to their non-changeable + # default file name. + # this is a bit more tricky since unique() will not work on list items + copyto_files_tmp = df['[copyto_files]'].astype(str) + copyto_files = [] + # cycle through the unique elements + for k in set(copyto_files_tmp): + # k is of form: "['some/file.txt', 'another/file1.txt']" + if len(k) < 2: + continue + items = [kk[1:-1] for kk in k.split('[')[1].split(']')[0].split(', ')] + copyto_files.extend(items) + # we might still have non unique elements + copyto_files = set(copyto_files) + for copyto_file, dst_rel in zip(copyto_files, df_dst): + src = os.path.join(run_dir, copyto_file) + # make dir if it does not exist + zf.write('.', os.path.dirname(copyto_file), '.') + zf.write(src, copyto_file) + + zf.close() + + return fname, df_index + + pbs_tmplate =""" +### Standard Output +#PBS -N [job_name] +#PBS -o [std_out] +### Standard Error +#PBS -e [std_err] +#PBS -W umask=[umask] +### Maximum wallclock time format HOURS:MINUTES:SECONDS +#PBS -l walltime=[walltime] +#PBS -l nodes=[nodes]:ppn=[ppn] +### Queue name +#PBS -q [queue] + +""" + + def make_pbs_chunks(df, ii, sim_id, run_dir, model_zip): + """Create a PBS that: + * copies all required files (zip chunk) to scratch disk + * copies all required turbulence files to scratch disk + * runs everything with find+xargs + * copies back what's need to mimer + """ +# ii = df.index[0] + cmd_find = '/home/MET/sysalt/bin/find' + cmd_xargs = '/home/MET/sysalt/bin/xargs' + jobid = '%s_chnk_%05i' % (sim_id, ii) + + pbase = os.path.join('/scratch','$USER', '$PBS_JOBID', '') + post_dir_base = post_dir.split(sim_id)[1] + if post_dir_base[0] == os.path.sep: + post_dir_base = post_dir_base[1:] + + pbs_in_base = os.path.commonpath(df['[pbs_in_dir]'].unique().tolist()) + pbs_in_base = os.path.join(pbs_in_base, '') + htc_base = os.path.commonpath(df['[htc_dir]'].unique().tolist()) + htc_base = os.path.join(htc_base, '') + res_base = os.path.commonpath(df['[res_dir]'].unique().tolist()) + res_base = os.path.join(res_base, '') + log_base = os.path.commonpath(df['[log_dir]'].unique().tolist()) + log_base = os.path.join(log_base, '') + + # ===================================================================== + # PBS HEADER + pbs = copy.copy(pbs_tmplate) + pbs = pbs.replace('[job_name]', jobid) + pbs = pbs.replace('[std_out]', './pbs_out_chunks/%s.out' % jobid) + pbs = pbs.replace('[std_err]', './pbs_out_chunks/%s.err' % jobid) + pbs = pbs.replace('[umask]', '0003') + pbs = pbs.replace('[walltime]', walltime) + pbs = pbs.replace('[nodes]', str(nodes)) + pbs = pbs.replace('[ppn]', str(ppn)) + pbs = pbs.replace('[queue]', queue) + pbs += '\necho "%s"\n' % ('-'*70) + + # ===================================================================== + # activate the python environment + pbs += 'echo "activate python environment %s"\n' % pyenv + pbs += 'source activate %s\n' % pyenv + # sometimes activating an environment fails due to a FileExistsError + # is this because it is activated at the same time on another node? + # check twice if the environment got activated for real + pbs += 'echo "CHECK 2x IF %s IS ACTIVE, IF NOT TRY AGAIN"\n' % pyenv + pbs += 'CMD=\"from distutils.sysconfig import get_python_lib;' + pbs += 'print (get_python_lib().find(\'%s\'))"\n' % pyenv + pbs += 'ACTIVATED=`python -c "$CMD"`\n' + pbs += 'if [ $ACTIVATED -eq -1 ]; then source activate %s;fi\n' % pyenv + pbs += 'ACTIVATED=`python -c "$CMD"`\n' + pbs += 'if [ $ACTIVATED -eq -1 ]; then source activate %s;fi\n' % pyenv + + # ===================================================================== + # create all necessary directories at CPU_NR dirs, turb db dirs, sim_id + # browse to scratch directory + pbs += '\necho "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % pbase + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n\n' + pbs += 'echo "create CPU directories on the scratch disk"\n' + pbs += 'mkdir -p %s\n' % os.path.join(pbase, sim_id, '') + for k in range(ppn): + pbs += 'mkdir -p %s\n' % os.path.join(pbase, '%i' % k, '') + # pretend to be on the scratch sim_id directory to maintain the same + # database turb structure + pbs += '\necho "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % os.path.join(pbase, sim_id, '') + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "create turb_db directories"\n' + db_dir_tags = ['[turb_db_dir]', '[meand_db_dir]', '[wake_db_dir]'] + turb_dirs = [] + for tag in db_dir_tags: + for dirname in set(df[tag].unique().tolist()): + if not dirname or dirname.lower() not in ['false', 'none']: + turb_dirs.append(dirname) + turb_dirs = set(turb_dirs) + for dirname in turb_dirs: + pbs += 'mkdir -p %s\n' % os.path.join(dirname, '') + + # ===================================================================== + # get the zip-chunk file from the PBS_O_WORKDIR + pbs += '\n' + pbs += 'echo "%s"\n' % ('-'*70) + pbs += 'cd $PBS_O_WORKDIR\n' + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "get the zip-chunk file from the PBS_O_WORKDIR"\n' + # copy the relevant zip chunk file to the scratch main directory + rpl = (os.path.join('./', chunks_dir, jobid), os.path.join(pbase, '')) + pbs += 'cp %s.zip %s\n' % rpl + + # turb_db_dir might not be set, same for turb_base_name, for those + # cases we do not need to copy anything from the database to the node + base_name_tags = ['[turb_base_name]', '[meand_base_name]', + '[wake_base_name]'] + for db, base_name in zip(db_dir_tags, base_name_tags): + turb_db_dirs = df[db] + df[base_name] + # When set to None, the DataFrame will have text as None + turb_db_src = turb_db_dirs[turb_db_dirs.str.find('None')==-1] + pbs += '\n' + pbs += '# copy to scratch db directory for %s, %s\n' % (db, base_name) + for k in turb_db_src.unique(): + dst = os.path.dirname(os.path.join(pbase, sim_id, k)) + pbs += 'cp %s* %s\n' % (k, os.path.join(dst, '.')) + + # ===================================================================== + # browse back to the scratch directory + pbs += '\necho "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % pbase + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "unzip chunk, create dirs in cpu and sim_id folders"\n' + # unzip chunk, this contains all relevant folders already, and also + # contains files defined in [copyto_files] + for k in list(range(ppn)) + [sim_id]: + dst = os.path.join('%s' % k, '.') + pbs += '/usr/bin/unzip %s -d %s >> /dev/null\n' % (jobid+'.zip', dst) + + # create hard links for all the turbulence files + turb_dir_base = os.path.join(os.path.commonpath(list(turb_dirs)), '') + pbs += '\necho "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % pbase + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "copy all turb files into CPU dirs"\n' + for k in range(ppn): + rpl = (os.path.relpath(os.path.join(sim_id, turb_dir_base)), k) + pbs += 'find %s -iname *.bin -exec cp {} %s/{} \\;\n' % rpl + + # ===================================================================== + # finally we can run find+xargs!!! + pbs += '\n' + pbs += 'echo "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % pbase + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "START RUNNING JOBS IN find+xargs MODE"\n' + pbs += 'WINEARCH=win32 WINEPREFIX=~/.wine32 winefix\n' + pbs += '# run all the PBS *.p files in find+xargs mode\n' + pbs += 'echo "following cases will be run from following path:"\n' + pbs += 'echo "%s"\n' % (os.path.join(sim_id, pbs_in_base)) + pbs += 'export LAUNCH_PBS_MODE=false\n' + rpl = (cmd_find, os.path.join(sim_id, pbs_in_base)) + pbs += "%s %s -type f -name '*.p' | sort -z\n" % rpl + pbs += '\n' + pbs += 'echo "number of files to be launched: "' + pbs += '`find %s -type f | wc -l`\n' % os.path.join(sim_id, pbs_in_base) + rpl = (cmd_find, os.path.join(sim_id, pbs_in_base), cmd_xargs, ppn) + cmd = ("%s %s -type f -name '*.p' -print0 | sort -z | %s -0 -I{} " + "--process-slot-var=CPU_NR -n 1 -P %i sh {}\n" % rpl) + pbs += cmd + pbs += 'echo "END OF JOBS IN find+xargs MODE"\n' + + # ===================================================================== + # move results back from the node sim_id dir to the origin + pbs += '\n' + pbs += '\necho "%s"\n' % ('-'*70) + pbs += "echo 'total scratch disk usage:'\n" + pbs += 'du -hs %s\n' % pbase + pbs += 'cd %s\n' % os.path.join(pbase, sim_id) + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "Results saved at sim_id directory:"\n' + rpl = (os.path.join(pbs_in_base, '*'), os.path.join(htc_base, '*')) + pbs += 'find \n' + + # compress all result files into an archive, first *.sel files + # FIXME: why doesn this work with -name "*.sel" -o -name "*.dat"?? + pbs += '\necho "move results into compressed archive"\n' + pbs += 'find %s -name "*.sel" -print0 ' % res_base + fname = os.path.join(res_base, 'resfiles_chnk_%05i' % ii) + pbs += '| xargs -0 tar --remove-files -rf %s.tar\n' % fname + # now add the *.dat files to the archive + pbs += 'find %s -name "*.dat" -print0 ' % res_base + fname = os.path.join(res_base, 'resfiles_chnk_%05i' % ii) + pbs += '| xargs -0 tar --remove-files -rf %s.tar\n' % fname + + pbs += 'xz -z2 -T %i %s.tar\n' % (ppn, fname) + + # compress all logfiles into an archive + pbs += '\necho "move logfiles into compressed archive"\n' + pbs += 'find %s -name "*.log" -print0 ' % log_base + fname = os.path.join(log_base, 'logfiles_chnk_%05i' % ii) + pbs += '| xargs -0 tar --remove-files -rf %s.tar\n' % fname + pbs += 'xz -z2 -T %i %s.tar\n' % (ppn, fname) + + # compress all post-processing results (saved as csv's) into an archive + pbs += '\necho "move statsdel into compressed archive"\n' + pbs += 'find %s -name "*.csv" -print0 ' % res_base + fname = os.path.join(post_dir_base, 'statsdel_chnk_%05i' % ii) + pbs += '| xargs -0 tar --remove-files -rf %s.tar\n' % fname + pbs += 'xz -z2 -T %i %s.tar\n' % (ppn, fname) + + # compress all post-processing results (saved as csv's) into an archive + pbs += '\necho "move log analysis into compressed archive"\n' + pbs += 'find %s -name "*.csv" -print0 ' % log_base + fname = os.path.join(post_dir_base, 'loganalysis_chnk_%05i' % ii) + pbs += '| xargs -0 tar --remove-files -rf %s.tar\n' % fname + pbs += 'xz -z2 -T %i %s.tar\n' % (ppn, fname) + + pbs += '\n' + pbs += '\necho "%s"\n' % ('-'*70) + pbs += 'cd %s\n' % pbase + pbs += "echo 'current working directory:'\n" + pbs += 'pwd\n' + pbs += 'echo "move results back from node scratch/sim_id to origin, ' + pbs += 'but ignore htc, and pbs_in directories."\n' + + tmp = os.path.join(sim_id, '*') + pbs += 'echo "copy from %s to $PBS_O_WORKDIR/"\n' % tmp + pbs += 'time rsync -au --remove-source-files %s $PBS_O_WORKDIR/ \\\n' % tmp + pbs += ' --exclude %s \\\n' % os.path.join(pbs_in_base, '*') + pbs += ' --exclude %s \n' % os.path.join(htc_base, '*') + # when using -u, htc and pbs_in files should be ignored +# pbs += 'time cp -ru %s $PBS_O_WORKDIR/\n' % tmp + pbs += 'source deactivate\n' + pbs += 'echo "DONE !!"\n' + pbs += '\necho "%s"\n' % ('-'*70) + pbs += '# in case wine has crashed, kill any remaining wine servers\n' + pbs += '# caution: ALL the users wineservers will die on this node!\n' + pbs += 'echo "following wineservers are still running:"\n' + pbs += 'ps -u $USER -U $USER | grep wineserver\n' + pbs += 'killall -u $USER wineserver\n' + pbs += 'exit\n' + + rpl = (sim_id, ii) + fname = os.path.join(run_dir, chunks_dir, '%s_chnk_%05i' % rpl) + with open(fname+'.p', 'w') as f: + f.write(pbs) + + def make_pbs_postpro_chunks(): + """When only the post-processing has to be re-done for a chunk. + """ + pass + + + cc = Cases(cases) + df = cc.cases2df() + # sort on the specified values in the given columns + df.sort_values(by=sort_by_values, inplace=True) + + # create the directory to store all zipped chunks + try: + os.mkdir(os.path.join(df['[run_dir]'].iloc[0], chunks_dir)) + # FIXME: how do you make this work pythonically on both PY2 and PY3? + except (FileExistsError, OSError): + pass + + df_iter = chunker(df, nr_procs_series*ppn) + sim_id = df['[sim_id]'].iloc[0] + run_dir = df['[run_dir]'].iloc[0] + model_zip = df['[model_zip]'].iloc[0] + post_dir = df['[post_dir]'].iloc[0] + nodes = 1 + df_ind = pd.DataFrame(columns=['chnk_nr'], dtype=np.int32) + df_ind.index.name = '[case_id]' + for ii, dfi in enumerate(df_iter): + fname, ind = make_zip_chunks(dfi, ii, sim_id, run_dir, model_zip) + make_pbs_chunks(dfi, ii, sim_id, run_dir, model_zip) + df_ind = df_ind.append(ind) + print(fname) + + fname = os.path.join(post_dir, 'case_id-chunk-index') + df_ind['chnk_nr'] = df_ind['chnk_nr'].astype(np.int32) + df_ind.to_hdf(fname+'.h5', 'table', compression=9, complib='zlib') + df_ind.to_csv(fname+'.csv') + + +def regroup_tarfiles(cc): + """Re-group all chunks again per [Case folder] compressed file. First all + chunks are copied to the node scratch disc, then start working on them. + This only works on a node with PBS stuff. + + Make sure to maintain the same location as defined by the tags! + + [res_dir] and [Case folder] could be multiple directories deep, bu the + final archive will only contain the files (no directory structure), and + the name of the archive is that of the last directory: + /[res_dir]/[Case folder]/[Case folder].tar.xz + /res/dir/case/folder/dlcname/dlcname.tar.xz + + Parameters + ---------- + + path_pattern : str + /path/to/files/*.tar.xz + + """ + + USER = os.getenv('USER') + PBS_JOBID = os.getenv('PBS_JOBID') + scratch = os.path.join('/scratch', USER, PBS_JOBID) + src = os.getenv('PBS_O_WORKDIR') + + path_pattern = '/home/dave/SimResults/NREL5MW/D0022/prepost-data/*.xz' + + for ffname in tqdm(glob.glob(path_pattern)): + appendix = os.path.basename(ffname).split('_')[0] + with tarfile.open(ffname, mode='r:xz') as tar: + # create new tar files if necessary for each [Case folder] + for tarinfo in tar.getmembers(): + t2_name = os.path.basename(os.path.dirname(tarinfo.name)) + t2_dir = os.path.join(os.path.dirname(path_pattern), t2_name) + if not os.path.isdir(t2_dir): + os.makedirs(t2_dir) + t2_path = os.path.join(t2_dir, t2_name + '_%s.tar' % appendix) + fileobj = tar.extractfile(tarinfo) + # change the location of the file in the new archive: + # the location of the archive is according to the folder + # structure as defined in the tags, remove any subfolders + tarinfo.name = os.basename(tarinfo.name) + with tarfile.open(t2_path, mode='a') as t2: + t2.addfile(tarinfo, fileobj) + + +def merge_from_tarfiles(df_fname, path, pattern, tarmode='r:xz', tqdm=False, + header='infer', names=None, sep=',', min_itemsize={}, + verbose=False, dtypes={}): + """Merge all csv files from various tar archives into a big pd.DataFrame + store. + + Parameters + ---------- + + df_fname : str + file name of the pd.DataFrame h5 store in which all chunks will be + merged. Names usually used are: + * [sim_id]_ErrorLogs.h5 + * [sim_id]_statistics.h5 + + path : str + Directory in which all chunks are located. + + pattern : str + Search pattern used to select (using glob.glob) files in path + + tarmode : str, default='r:xz' + File opening mode for tarfile (used when opening each of the chunks). + + tqdm : boolean, default=False + If True, an interactive progress bar will be displayed (requires the + tqdm module). If set to False no progress bar will be displayed. + + header : int, default='infer' + Argument passed on to pandas.read_csv. Default to 'infer', set to + None if there is no header, set to 0 if header is on first row. + + names : list of column names, default=None + Argument passed on to pandas.read_csv. Default to None. List with + column names to be used in the DataFrame. + + min_itemsize : dict, default={} + Argument passed on to pandas.HDFStore.append. Set the minimum lenght + for a given column in the DataFrame. + + sep : str, default=',' + Argument passed on to pandas.read_csv. Set to ';' when handling the + ErrorLogs. + + """ + + store = pd.HDFStore(os.path.join(path, df_fname), mode='w', format='table', + complevel=9, complib='zlib') + + if tqdm: + from tqdm import tqdm + else: + def tqdm(itereable): + return itereable + + for tar_fname in tqdm(glob.glob(os.path.join(path, pattern))): + if verbose: + print(tar_fname) + with tarfile.open(tar_fname, mode=tarmode) as tar: + df = pd.DataFrame() + for tarinfo in tar.getmembers(): + fileobj = tar.extractfile(tarinfo) + tmp = pd.read_csv(fileobj, header=header, names=names, sep=sep) + for col, dtype in dtypes.items(): + tmp[col] = tmp[col].astype(dtype) + df = df.append(tmp) + try: + if verbose: + print('writing...') + store.append('table', df, min_itemsize=min_itemsize) + except Exception as e: + if verbose: + print('store columns:') + print(store.select('table', start=0, stop=0).columns) + print('columns of the DataFrame being added:') + print(df.columns) + storecols = store.select('table', start=0, stop=0).columns + store.close() + print(e) + return df, storecols + + store.close() + return None, None + + +# TODO: make this class more general so you can also just give a list of files +# to be merged, excluding the tar archives. +class AppendDataFrames(object): + """Merge DataFrames, either in h5 or csv format, located in (compressed) + tar archives. + """ + + def __init__(self, tqdm=False): + if tqdm: + from tqdm import tqdm + else: + def tqdm(itereable): + return itereable + self.tqdm = tqdm + + def df2store(self, store, path, tarmode='r:xz', min_itemsize={}, + colnames=None, header='infer', columns=None, sep=';', + index2col=None, ignore_index=True, fname_col=False): + """ + """ + + # TODO: it seems that with threading you could parallelize this kind + # of work: http://stackoverflow.com/q/23598063/3156685 + # http://stackoverflow.com/questions/23598063/ + # multithreaded-web-scraper-to-store-values-to-pandas-dataframe + + # http://gouthamanbalaraman.com/blog/distributed-processing-pandas.html + + df = pd.DataFrame() + for fname in self.tqdm(glob.glob(path)): + with tarfile.open(fname, mode=tarmode) as tar: + df = pd.DataFrame() + for tarinfo in tar.getmembers(): + fileobj = tar.extractfile(tarinfo) + if tarinfo.name[-2:] == 'h5': + tmp = pd.read_hdf(fileobj, 'table', columns=columns) + elif tarinfo.name[-3:] == 'csv': + tmp = pd.read_csv(fileobj, sep=sep, names=colnames, + header=header, usecols=columns) + else: + continue + if index2col is not None: + # if the index does not have a name we can still set it + tmp[index2col] = tmp.index + tmp[index2col] = tmp[index2col].astype(str) + tmp.reset_index(level=0, drop=True, inplace=True) + # add the file name as a column + if fname_col: + case_id = os.path.basename(tarinfo.name) + tmp[fname_col] = '.'.join(case_id.split('.')[:-1]) + tmp[fname_col] = tmp[fname_col].astype(str) + df = df.append(tmp, ignore_index=ignore_index) + + store.append('table', df, min_itemsize=min_itemsize) +# if len(df) > w_every: +# # and merge into the big ass DataFrame +# store.append('table', df, min_itemsize=min_itemsize) +# df = pd.DataFrame() + return store + + # FIXME: when merging log file analysis (files with header), we are still + # skipping over one case + def txt2txt(self, fjoined, path, tarmode='r:xz', header=None, sep=';', + fname_col=False): + """Read as strings, write to another file as strings. + """ + if header is not None: + write_header = True + icut = header + 1 + else: + # when header is None, there is no header + icut = 0 + write_header = False + with open(fjoined, 'w') as f: + for fname in self.tqdm(glob.glob(path)): + with tarfile.open(fname, mode=tarmode) as tar: + for tarinfo in tar.getmembers(): + linesb = tar.extractfile(tarinfo).readlines() + # convert from bytes to strings + lines = [line.decode() for line in linesb] + # only include the header at the first round + if write_header: + line = lines[header] + # add extra column with the file name if applicable + if fname_col: + rpl = sep + fname_col + '\n' + line = line.replace('\n', rpl) + f.write(line) + write_header = False + # but cut out the header on all other occurances + for line in lines[icut:]: + if fname_col: + case_id = os.path.basename(tarinfo.name) + case_id = '.'.join(case_id.split('.')[:-1]) + line = line.replace('\n', sep + case_id + '\n') + f.write(line) + f.flush() + + def csv2df_chunks(self, store, fcsv, chunksize=100000, min_itemsize={}, + colnames=None, dtypes={}, header='infer', sep=';'): + """Convert a large csv file to a pandas.DataFrame in chunks using + a pandas.HDFStore. + """ + df_iter = pd.read_csv(fcsv, chunksize=chunksize, sep=sep, + names=colnames, header=header) + for df_chunk in self.tqdm(df_iter): + for col, dtype in dtypes.items(): + df_chunk[col] = df_chunk[col].astype(dtype) + store.append('table', df_chunk, min_itemsize=min_itemsize) + return store + + +if __name__ == '__main__': + pass diff --git a/wetb/prepost/statsdel.py b/wetb/prepost/statsdel.py new file mode 100644 index 0000000000000000000000000000000000000000..269198115556920bb4d8bdf77be4703bd0355e18 --- /dev/null +++ b/wetb/prepost/statsdel.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 21 15:13:38 2016 + +@author: dave +""" + +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals +from __future__ import absolute_import +#from builtins import dict +#from io import open +#from builtins import zip +#from builtins import range +#from builtins import str +#from builtins import int +from future import standard_library +standard_library.install_aliases() + +import os + +from wetb.prepost import windIO + + +def logcheck(fname, fsave=None, mode='w'): + """Check the log file of a single HAWC2 simulation and save results to + textfile. + """ + + logf = windIO.LogFile() + logf.readlog(fname) + contents = logf._msglistlog2csv('') + if fsave is None: + fsave = fname.replace('.log', '.csv') + with open(fsave, mode) as f: + f.write(contents) + + +def calc(fpath, no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=None, i0=0, i1=None, + ftype=False, fsave=False): + """ + Should we load m, statchans, delchans from another file? This function + will be called from a PBS script. + """ + + if fpath[-4:] == '.sel' or fpath[-4:] == '.dat': + fpath = fpath[-4:] + + fdir = os.path.dirname(fpath) + fname = os.path.basename(fpath) + + res = windIO.LoadResults(fdir, fname, debug=False, usecols=None, + readdata=True) + statsdel = res.statsdel_df(i0=i0, i1=i1, statchans='all', neq=neq, + no_bins=no_bins, m=m, delchans='all') + + if fsave: + if fsave[-4:] == '.csv': + statsdel.to_csv(fsave) + elif fsave[-3:] == '.h5': + statsdel.to_hdf(fsave, 'table', complib='zlib', complevel=9) + elif ftype == '.csv': + statsdel.to_csv(fpath+ftype) + elif ftype == '.h5': + statsdel.to_hdf(fpath+ftype, 'table', complib='zlib', complevel=9) + + return res, statsdel + + +if __name__ == '__main__': + pass diff --git a/wetb/prepost/tests/data/a_phi_0.00000_shear_-0.20_wdir00.txt b/wetb/prepost/tests/data/a_phi_0.00000_shear_-0.20_wdir00.txt new file mode 100644 index 0000000000000000000000000000000000000000..e3b9b9994b05989fa67c8f4cc7bf4810fad1c9bd --- /dev/null +++ b/wetb/prepost/tests/data/a_phi_0.00000_shear_-0.20_wdir00.txt @@ -0,0 +1,30 @@ +# User defined shear file +3 5 # nr_hor (v), nr_vert (w) +# v component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# u component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 1.20491 1.20491 1.20491 + 1.04894 1.04894 1.04894 + 0.96723 0.96723 0.96723 + 0.91315 0.91315 0.91315 +# w component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# v coordinates (along the horizontal, nr_hor, 0 rotor center) + -57.50 + 0.00 + 57.50 +# w coordinates (zero is at ground level, height, nr_hor) + 0.00 + 39.38 + 78.75 + 118.12 + 157.50 diff --git a/wetb/prepost/tests/data/a_phi_0.00000_shear_0.00_wdir-10.txt b/wetb/prepost/tests/data/a_phi_0.00000_shear_0.00_wdir-10.txt new file mode 100644 index 0000000000000000000000000000000000000000..30c83777bab7a89d02f8d332f18188093f2289c0 --- /dev/null +++ b/wetb/prepost/tests/data/a_phi_0.00000_shear_0.00_wdir-10.txt @@ -0,0 +1,30 @@ +# User defined shear file +3 5 # nr_hor (v), nr_vert (w) +# v component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.17365 0.17365 0.17365 + 0.17365 0.17365 0.17365 + 0.17365 0.17365 0.17365 + 0.17365 0.17365 0.17365 + 0.17365 0.17365 0.17365 +# u component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.98481 0.98481 0.98481 + 0.98481 0.98481 0.98481 + 0.98481 0.98481 0.98481 + 0.98481 0.98481 0.98481 + 0.98481 0.98481 0.98481 +# w component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# v coordinates (along the horizontal, nr_hor, 0 rotor center) + -57.50 + 0.00 + 57.50 +# w coordinates (zero is at ground level, height, nr_hor) + 0.00 + 39.38 + 78.75 + 118.12 + 157.50 diff --git a/wetb/prepost/tests/data/a_phi_0.50000_shear_0.20_wdir10.txt b/wetb/prepost/tests/data/a_phi_0.50000_shear_0.20_wdir10.txt new file mode 100644 index 0000000000000000000000000000000000000000..48da580f70591f4a745a7c848b524d433af8dd80 --- /dev/null +++ b/wetb/prepost/tests/data/a_phi_0.50000_shear_0.20_wdir10.txt @@ -0,0 +1,30 @@ +# User defined shear file +3 5 # nr_hor (v), nr_vert (w) +# v component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns +-0.00000 -0.00000 -0.00000 +-0.22662 -0.22662 -0.22662 +-0.19440 -0.19440 -0.19440 +-0.15702 -0.15702 -0.15702 +-0.12881 -0.12881 -0.12881 +# u component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.79839 0.79839 0.79839 + 0.93331 0.93331 0.93331 + 1.02188 1.02188 1.02188 + 1.08750 1.08750 1.08750 +# w component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# v coordinates (along the horizontal, nr_hor, 0 rotor center) + -57.50 + 0.00 + 57.50 +# w coordinates (zero is at ground level, height, nr_hor) + 0.00 + 39.38 + 78.75 + 118.12 + 157.50 diff --git a/wetb/prepost/tests/data/a_phi_1.00000_shear_0.00_wdir00.txt b/wetb/prepost/tests/data/a_phi_1.00000_shear_0.00_wdir00.txt new file mode 100644 index 0000000000000000000000000000000000000000..90624d10f1562909b9ca4117ab4988da968c32c2 --- /dev/null +++ b/wetb/prepost/tests/data/a_phi_1.00000_shear_0.00_wdir00.txt @@ -0,0 +1,30 @@ +# User defined shear file +3 5 # nr_hor (v), nr_vert (w) +# v component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns +-0.37387 -0.37387 -0.37387 +-0.20267 -0.20267 -0.20267 +-0.06161 -0.06161 -0.06161 + 0.04413 0.04413 0.04413 + 0.11303 0.11303 0.11303 +# u component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.92748 0.92748 0.92748 + 0.97925 0.97925 0.97925 + 0.99810 0.99810 0.99810 + 0.99903 0.99903 0.99903 + 0.99359 0.99359 0.99359 +# w component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# v coordinates (along the horizontal, nr_hor, 0 rotor center) + -57.50 + 0.00 + 57.50 +# w coordinates (zero is at ground level, height, nr_hor) + 0.00 + 39.38 + 78.75 + 118.12 + 157.50 diff --git a/wetb/prepost/tests/data/a_phi_None_shear_None_wdirNone.txt b/wetb/prepost/tests/data/a_phi_None_shear_None_wdirNone.txt new file mode 100644 index 0000000000000000000000000000000000000000..536a05286b078493c85480a6f038e2f064118620 --- /dev/null +++ b/wetb/prepost/tests/data/a_phi_None_shear_None_wdirNone.txt @@ -0,0 +1,30 @@ +# User defined shear file +3 5 # nr_hor (v), nr_vert (w) +# v component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# u component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 1.00000 1.00000 1.00000 + 1.00000 1.00000 1.00000 + 1.00000 1.00000 1.00000 + 1.00000 1.00000 1.00000 + 1.00000 1.00000 1.00000 +# w component, normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 + 0.00000 0.00000 0.00000 +# v coordinates (along the horizontal, nr_hor, 0 rotor center) + -57.50 + 0.00 + 57.50 +# w coordinates (zero is at ground level, height, nr_hor) + 0.00 + 39.38 + 78.75 + 118.12 + 157.50 diff --git a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp10_s100.p b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp10_s100.p index 6a62ecceb1be436ba34a622fdb95db9f66890924..8747810f9d82fb4a89293e4169784321fc168138 100644 --- a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp10_s100.p +++ b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp10_s100.p @@ -3,53 +3,113 @@ #PBS -o ./pbs_out/dlc01_demos/dlc01_steady_wsp10_s100.out ### Standard Error #PBS -e ./pbs_out/dlc01_demos/dlc01_steady_wsp10_s100.err -#PBS -W umask=003 +#PBS -W umask=0003 ### Maximum wallclock time format HOURS:MINUTES:SECONDS #PBS -l walltime=04:00:00 #PBS -l nodes=1:ppn=1 ### Queue name #PBS -q workq -### Create scratch directory and copy data to it -cd $PBS_O_WORKDIR -echo "current working dir (pwd):" -pwd -cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +# ============================================================================== +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### Create scratch directory and copy data to it + cd $PBS_O_WORKDIR + echo "current working dir (pwd):" + pwd + cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +fi +# ------------------------------------------------------------------------------ -echo "" -echo "Execute commands on scratch nodes" -cd /scratch/$USER/$PBS_JOBID -pwd -/usr/bin/unzip demo_dlc_remote.zip -mkdir -p htc/dlc01_demos/ -mkdir -p res/dlc01_demos/ -mkdir -p logfiles/dlc01_demos/ -mkdir -p turb/ -cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp10_s100.htc ./htc/dlc01_demos/ -cp -R $PBS_O_WORKDIR/../turb/turb_s100_10ms*.bin turb/ -time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp10_s100.htc & -### wait for jobs to finish -wait -echo "" -echo "Copy back from scratch directory" -cd /scratch/$USER/$PBS_JOBID -mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/animation/ -mkdir -p $PBS_O_WORKDIR/../turb/ -cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. -cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. -cp -R animation/. $PBS_O_WORKDIR/animation/. +# ------------------------------------------------------------ +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo + echo 'Execute commands on scratch nodes' + cd /scratch/$USER/$PBS_JOBID + # create unique dir for each CPU + mkdir "1"; cd "1" + pwd + /usr/bin/unzip ../demo_dlc_remote.zip + mkdir -p htc/dlc01_demos/ + mkdir -p res/dlc01_demos/ + mkdir -p logfiles/dlc01_demos/ + mkdir -p turb/ + cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp10_s100.htc ./htc/dlc01_demos/ + cp -R $PBS_O_WORKDIR/../turb/turb_s100_10ms*.bin turb/ + # ------------------------------------------------------------ +else + # with find+xargs we first browse to CPU folder + cd "$CPU_NR" +fi +# ------------------------------------------------------------ echo "" -echo "COPY BACK TURB IF APPLICABLE" -cd turb/ -for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done -cd /scratch/$USER/$PBS_JOBID -echo "END COPY BACK TURB" -echo "" +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo "execute HAWC2, fork to background" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp10_s100.htc & + wait +else + echo "execute HAWC2, do not fork and wait" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp10_s100.htc + echo "POST-PROCESSING" + python -c "from wetb.prepost import statsdel; statsdel.logcheck('logfiles/dlc01_demos/dlc01_steady_wsp10_s100.log')" + python -c "from wetb.prepost import statsdel; statsdel.calc('res/dlc01_demos/dlc01_steady_wsp10_s100', no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=20.0, i0=0, i1=None, ftype='.csv')" +fi -echo "" -echo "following files are on the node (find .):" -find . + +# ============================================================================== +### Epilogue +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### wait for jobs to finish + wait + echo "" +# ------------------------------------------------------------------------------ + echo "Copy back from scratch directory" + cd /scratch/$USER/$PBS_JOBID/1/ + mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/animation/ + mkdir -p $PBS_O_WORKDIR/../turb/ + cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. + cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. + cp -R animation/. $PBS_O_WORKDIR/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/1/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + + echo "" + echo "following files are on node/cpu 1 (find .):" + find . +# ------------------------------------------------------------------------------ +else + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + rsync -a --remove-source-files res/dlc01_demos/. ../remote/res/dlc01_demos/. + rsync -a --remove-source-files logfiles/dlc01_demos/. ../remote/logfiles/dlc01_demos/. + rsync -a --remove-source-files animation/. ../remote/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e ../../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i ../../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + +# ------------------------------------------------------------------------------ +fi exit diff --git a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp8_noturb.p b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp8_noturb.p index 640e5c20ec09f6f50b5a6002586bd030b96604c9..14c07d42e55b395c945458b10ff83325bdba6ec3 100644 --- a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp8_noturb.p +++ b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp8_noturb.p @@ -3,53 +3,113 @@ #PBS -o ./pbs_out/dlc01_demos/dlc01_steady_wsp8_noturb.out ### Standard Error #PBS -e ./pbs_out/dlc01_demos/dlc01_steady_wsp8_noturb.err -#PBS -W umask=003 +#PBS -W umask=0003 ### Maximum wallclock time format HOURS:MINUTES:SECONDS #PBS -l walltime=04:00:00 #PBS -l nodes=1:ppn=1 ### Queue name #PBS -q workq -### Create scratch directory and copy data to it -cd $PBS_O_WORKDIR -echo "current working dir (pwd):" -pwd -cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +# ============================================================================== +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### Create scratch directory and copy data to it + cd $PBS_O_WORKDIR + echo "current working dir (pwd):" + pwd + cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +fi +# ------------------------------------------------------------------------------ -echo "" -echo "Execute commands on scratch nodes" -cd /scratch/$USER/$PBS_JOBID -pwd -/usr/bin/unzip demo_dlc_remote.zip -mkdir -p htc/dlc01_demos/ -mkdir -p res/dlc01_demos/ -mkdir -p logfiles/dlc01_demos/ -mkdir -p turb/ -cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp8_noturb.htc ./htc/dlc01_demos/ -cp -R $PBS_O_WORKDIR/../turb/none*.bin turb/ -time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp8_noturb.htc & -### wait for jobs to finish -wait -echo "" -echo "Copy back from scratch directory" -cd /scratch/$USER/$PBS_JOBID -mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/animation/ -mkdir -p $PBS_O_WORKDIR/../turb/ -cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. -cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. -cp -R animation/. $PBS_O_WORKDIR/animation/. +# ------------------------------------------------------------ +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo + echo 'Execute commands on scratch nodes' + cd /scratch/$USER/$PBS_JOBID + # create unique dir for each CPU + mkdir "1"; cd "1" + pwd + /usr/bin/unzip ../demo_dlc_remote.zip + mkdir -p htc/dlc01_demos/ + mkdir -p res/dlc01_demos/ + mkdir -p logfiles/dlc01_demos/ + mkdir -p turb/ + cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp8_noturb.htc ./htc/dlc01_demos/ + cp -R $PBS_O_WORKDIR/../turb/none*.bin turb/ + # ------------------------------------------------------------ +else + # with find+xargs we first browse to CPU folder + cd "$CPU_NR" +fi +# ------------------------------------------------------------ echo "" -echo "COPY BACK TURB IF APPLICABLE" -cd turb/ -for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done -cd /scratch/$USER/$PBS_JOBID -echo "END COPY BACK TURB" -echo "" +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo "execute HAWC2, fork to background" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp8_noturb.htc & + wait +else + echo "execute HAWC2, do not fork and wait" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp8_noturb.htc + echo "POST-PROCESSING" + python -c "from wetb.prepost import statsdel; statsdel.logcheck('logfiles/dlc01_demos/dlc01_steady_wsp8_noturb.log')" + python -c "from wetb.prepost import statsdel; statsdel.calc('res/dlc01_demos/dlc01_steady_wsp8_noturb', no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=20.0, i0=0, i1=None, ftype='.csv')" +fi -echo "" -echo "following files are on the node (find .):" -find . + +# ============================================================================== +### Epilogue +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### wait for jobs to finish + wait + echo "" +# ------------------------------------------------------------------------------ + echo "Copy back from scratch directory" + cd /scratch/$USER/$PBS_JOBID/1/ + mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/animation/ + mkdir -p $PBS_O_WORKDIR/../turb/ + cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. + cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. + cp -R animation/. $PBS_O_WORKDIR/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/1/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + + echo "" + echo "following files are on node/cpu 1 (find .):" + find . +# ------------------------------------------------------------------------------ +else + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + rsync -a --remove-source-files res/dlc01_demos/. ../remote/res/dlc01_demos/. + rsync -a --remove-source-files logfiles/dlc01_demos/. ../remote/logfiles/dlc01_demos/. + rsync -a --remove-source-files animation/. ../remote/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e ../../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i ../../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + +# ------------------------------------------------------------------------------ +fi exit diff --git a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp9_noturb.p b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp9_noturb.p index a9d5ef524b9feddbadd7df3e08d7873cc39fe96d..879196497b947b19454fd3bf5addd78ac5365612 100644 --- a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp9_noturb.p +++ b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in/dlc01_demos/dlc01_steady_wsp9_noturb.p @@ -3,53 +3,113 @@ #PBS -o ./pbs_out/dlc01_demos/dlc01_steady_wsp9_noturb.out ### Standard Error #PBS -e ./pbs_out/dlc01_demos/dlc01_steady_wsp9_noturb.err -#PBS -W umask=003 +#PBS -W umask=0003 ### Maximum wallclock time format HOURS:MINUTES:SECONDS #PBS -l walltime=04:00:00 #PBS -l nodes=1:ppn=1 ### Queue name #PBS -q workq -### Create scratch directory and copy data to it -cd $PBS_O_WORKDIR -echo "current working dir (pwd):" -pwd -cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +# ============================================================================== +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### Create scratch directory and copy data to it + cd $PBS_O_WORKDIR + echo "current working dir (pwd):" + pwd + cp -R ./demo_dlc_remote.zip /scratch/$USER/$PBS_JOBID +fi +# ------------------------------------------------------------------------------ -echo "" -echo "Execute commands on scratch nodes" -cd /scratch/$USER/$PBS_JOBID -pwd -/usr/bin/unzip demo_dlc_remote.zip -mkdir -p htc/dlc01_demos/ -mkdir -p res/dlc01_demos/ -mkdir -p logfiles/dlc01_demos/ -mkdir -p turb/ -cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp9_noturb.htc ./htc/dlc01_demos/ -cp -R $PBS_O_WORKDIR/../turb/none*.bin turb/ -time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp9_noturb.htc & -### wait for jobs to finish -wait -echo "" -echo "Copy back from scratch directory" -cd /scratch/$USER/$PBS_JOBID -mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ -mkdir -p $PBS_O_WORKDIR/animation/ -mkdir -p $PBS_O_WORKDIR/../turb/ -cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. -cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. -cp -R animation/. $PBS_O_WORKDIR/animation/. +# ------------------------------------------------------------ +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo + echo 'Execute commands on scratch nodes' + cd /scratch/$USER/$PBS_JOBID + # create unique dir for each CPU + mkdir "1"; cd "1" + pwd + /usr/bin/unzip ../demo_dlc_remote.zip + mkdir -p htc/dlc01_demos/ + mkdir -p res/dlc01_demos/ + mkdir -p logfiles/dlc01_demos/ + mkdir -p turb/ + cp -R $PBS_O_WORKDIR/htc/dlc01_demos/dlc01_steady_wsp9_noturb.htc ./htc/dlc01_demos/ + cp -R $PBS_O_WORKDIR/../turb/none*.bin turb/ + # ------------------------------------------------------------ +else + # with find+xargs we first browse to CPU folder + cd "$CPU_NR" +fi +# ------------------------------------------------------------ echo "" -echo "COPY BACK TURB IF APPLICABLE" -cd turb/ -for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done -cd /scratch/$USER/$PBS_JOBID -echo "END COPY BACK TURB" -echo "" +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + echo "execute HAWC2, fork to background" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp9_noturb.htc & + wait +else + echo "execute HAWC2, do not fork and wait" + time WINEARCH=win32 WINEPREFIX=~/.wine32 wine hawc2-latest ./htc/dlc01_demos/dlc01_steady_wsp9_noturb.htc + echo "POST-PROCESSING" + python -c "from wetb.prepost import statsdel; statsdel.logcheck('logfiles/dlc01_demos/dlc01_steady_wsp9_noturb.log')" + python -c "from wetb.prepost import statsdel; statsdel.calc('res/dlc01_demos/dlc01_steady_wsp9_noturb', no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=20.0, i0=0, i1=None, ftype='.csv')" +fi -echo "" -echo "following files are on the node (find .):" -find . + +# ============================================================================== +### Epilogue +# evaluates to true if LAUNCH_PBS_MODE is NOT set +if [ -z ${LAUNCH_PBS_MODE+x} ] ; then + ### wait for jobs to finish + wait + echo "" +# ------------------------------------------------------------------------------ + echo "Copy back from scratch directory" + cd /scratch/$USER/$PBS_JOBID/1/ + mkdir -p $PBS_O_WORKDIR/res/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/logfiles/dlc01_demos/ + mkdir -p $PBS_O_WORKDIR/animation/ + mkdir -p $PBS_O_WORKDIR/../turb/ + cp -R res/dlc01_demos/. $PBS_O_WORKDIR/res/dlc01_demos/. + cp -R logfiles/dlc01_demos/. $PBS_O_WORKDIR/logfiles/dlc01_demos/. + cp -R animation/. $PBS_O_WORKDIR/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e $PBS_O_WORKDIR/../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i $PBS_O_WORKDIR/../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/1/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + + echo "" + echo "following files are on node/cpu 1 (find .):" + find . +# ------------------------------------------------------------------------------ +else + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + rsync -a --remove-source-files res/dlc01_demos/. ../remote/res/dlc01_demos/. + rsync -a --remove-source-files logfiles/dlc01_demos/. ../remote/logfiles/dlc01_demos/. + rsync -a --remove-source-files animation/. ../remote/animation/. + + echo "" + echo "COPY BACK TURB IF APPLICABLE" + cd turb/ + for i in `ls *.bin`; do if [ -e ../../turb/$i ]; then echo "$i exists no copyback"; else echo "$i copyback"; cp $i ../../turb/; fi; done + cd /scratch/$USER/$PBS_JOBID/$CPU_NR/ + echo "END COPY BACK TURB" + echo "" + + echo "COPYBACK [copyback_files]/[copyback_frename]" + echo "END COPYBACK" + echo "" + +# ------------------------------------------------------------------------------ +fi exit diff --git a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in_turb/turb_s100_10ms.p b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in_turb/turb_s100_10ms.p index df16aba35748c37417076913bbd9b514fce34301..d16fe92e9502d1d340173ba6a5605d5ac937b596 100644 --- a/wetb/prepost/tests/data/demo_dlc/ref/pbs_in_turb/turb_s100_10ms.p +++ b/wetb/prepost/tests/data/demo_dlc/ref/pbs_in_turb/turb_s100_10ms.p @@ -4,7 +4,7 @@ #PBS -o ./pbs_out/turb/turb_s100_10ms.out ### Standard Error #PBS -e ./pbs_out/turb/turb_s100_10ms.err -#PBS -W umask=003 +#PBS -W umask=0003 ### Maximum wallclock time format HOURS:MINUTES:SECONDS #PBS -l walltime=00:59:59 #PBS -lnodes=1:ppn=1 diff --git a/wetb/prepost/tests/test_Simulations.py b/wetb/prepost/tests/test_Simulations.py index a439a53e04a92992cab62e9637ce8a11ad5e3e89..b6ea82859fdef30b0dde5edc01a83dd429e13e01 100644 --- a/wetb/prepost/tests/test_Simulations.py +++ b/wetb/prepost/tests/test_Simulations.py @@ -48,7 +48,8 @@ class TestGenerateInputs(unittest.TestCase): shutil.rmtree(os.path.join(p_root, tmpl.PROJECT, 'remote')) tmpl.force_dir = tmpl.P_RUN - tmpl.launch_dlcs_excel('remote', silent=True, runmethod='gorm') + tmpl.launch_dlcs_excel('remote', silent=True, runmethod='gorm', + pbs_turb=True) # we can not check-in empty dirs so we can not compare the complete # directory structure withouth manually creating the empty dirs here diff --git a/wetb/prepost/tests/test_windIO.py b/wetb/prepost/tests/test_windIO.py index 45cce4c63dd146e8ccd6dd79cb545f41a83a0503..84c5a9d551830903ac8b52fb1be0791954e5bea2 100644 --- a/wetb/prepost/tests/test_windIO.py +++ b/wetb/prepost/tests/test_windIO.py @@ -12,10 +12,11 @@ standard_library.install_aliases() import unittest import os +import tempfile import numpy as np -from wetb.prepost.windIO import LoadResults +from wetb.prepost import windIO class TestsLoadResults(unittest.TestCase): @@ -27,7 +28,7 @@ class TestsLoadResults(unittest.TestCase): self.fbin = 'Hawc2bin' def loadresfile(self, resfile): - res = LoadResults(self.respath, resfile) + res = windIO.LoadResults(self.respath, resfile) self.assertTrue(hasattr(res, 'sig')) self.assertEqual(res.Freq, 40.0) self.assertEqual(res.N, 800) @@ -45,15 +46,15 @@ class TestsLoadResults(unittest.TestCase): self.assertEqual(res.FileType, 'BINARY') def test_compare_ascii_bin(self): - res_ascii = LoadResults(self.respath, self.fascii) - res_bin = LoadResults(self.respath, self.fbin) + res_ascii = windIO.LoadResults(self.respath, self.fascii) + res_bin = windIO.LoadResults(self.respath, self.fbin) for k in range(res_ascii.sig.shape[1]): np.testing.assert_allclose(res_ascii.sig[:,k], res_bin.sig[:,k], rtol=1e-02, atol=0.001) def test_unified_chan_names(self): - res = LoadResults(self.respath, self.fascii, readdata=False) + res = windIO.LoadResults(self.respath, self.fascii, readdata=False) self.assertFalse(hasattr(res, 'sig')) np.testing.assert_array_equal(res.ch_df.index.values, np.arange(0,28)) @@ -62,5 +63,62 @@ class TestsLoadResults(unittest.TestCase): 'windspeed-global-Vy--2.50-1.00--52.50') +class TestUserWind(unittest.TestCase): + + def setUp(self): + self.path = os.path.join(os.path.dirname(__file__), 'data') + self.z_h = 100.0 + self.r_blade_tip = 50.0 + self.h_ME = 650 + self.z = np.array([self.z_h - self.r_blade_tip, + self.z_h + self.r_blade_tip]) + + def test_deltaphi2aphi(self): + + uwind = windIO.UserWind() + profiles = windIO.WindProfiles + + for a_phi_ref in [-1.0, 0.0, 0.5]: + phis = profiles.veer_ekman_mod(self.z, self.z_h, h_ME=self.h_ME, + a_phi=a_phi_ref) + d_phi_ref = phis[1] - phis[0] +# a_phi1 = uwind.deltaphi2aphi(d_phi_ref, self.z_h, self.r_blade_tip, +# h_ME=self.h_ME) + a_phi2 = uwind.deltaphi2aphi_opt(d_phi_ref, self.z, self.z_h, + self.r_blade_tip, self.h_ME) + self.assertAlmostEqual(a_phi_ref, a_phi2) + + def test_usershear(self): + + uwind = windIO.UserWind() + + # phi, shear, wdir + combinations = [[1,0,0], [0,-0.2,0], [0,0,-10], [None, None, None], + [0.5,0.2,10]] + + for a_phi, shear, wdir in combinations: + rpl = (a_phi, shear, wdir) + try: + fname = 'a_phi_%1.05f_shear_%1.02f_wdir%02i.txt' % rpl + except: + fname = 'a_phi_%s_shear_%s_wdir%s.txt' % rpl + target = os.path.join(self.path, fname) + + fid = tempfile.NamedTemporaryFile(delete=False, mode='wb') + target = os.path.join(self.path, fname) + uu, vv, ww, xx, zz = uwind(self.z_h, self.r_blade_tip, a_phi=a_phi, + nr_vert=5, nr_hor=3, h_ME=650.0, + wdir=wdir, io=fid, shear_exp=shear) + # FIXME: this has to be done more clean and Pythonic + # load again for comparison with the reference + uwind.fid.close() + with open(uwind.fid.name) as fid: + contents = fid.readlines() + os.remove(uwind.fid.name) + with open(target) as fid: + ref = fid.readlines() + self.assertEqual(contents, ref) + + if __name__ == "__main__": unittest.main() diff --git a/wetb/prepost/windIO.py b/wetb/prepost/windIO.py index dec84605b28b0b1cd3c5972111e28a36f7c24e38..7571cb17dd2d3ce8814d6d19a323b2a98de4163b 100755 --- a/wetb/prepost/windIO.py +++ b/wetb/prepost/windIO.py @@ -27,9 +27,11 @@ import struct import math from time import time import codecs +from itertools import chain -import scipy.integrate as integrate import numpy as np +import scipy as sp +import scipy.integrate as integrate import pandas as pd # misc is part of prepost, which is available on the dtu wind gitlab server: @@ -41,6 +43,410 @@ from wetb.hawc2.Hawc2io import ReadHawc2 from wetb.fatigue_tools.fatigue import eq_load +class LogFile(object): + """Check a HAWC2 log file for errors. + """ + + def __init__(self): + + # the total message list log: + self.MsgListLog = [] + # a smaller version, just indication if there are errors: + self.MsgListLog2 = dict() + + # specify which message to look for. The number track's the order. + # this makes it easier to view afterwards in spreadsheet: + # every error will have its own column + + # error messages that appear during initialisation + self.err_init = {} + self.err_init[' *** ERROR *** Error in com'] = len(self.err_init) + self.err_init[' *** ERROR *** in command '] = len(self.err_init) + # *** WARNING *** A comma "," is written within the command line + self.err_init[' *** WARNING *** A comma ",'] = len(self.err_init) + # *** ERROR *** Not correct number of parameters + self.err_init[' *** ERROR *** Not correct '] = len(self.err_init) + # *** INFO *** End of file reached + self.err_init[' *** INFO *** End of file r'] = len(self.err_init) + # *** ERROR *** No line termination in command line + self.err_init[' *** ERROR *** No line term'] = len(self.err_init) + # *** ERROR *** MATRIX IS NOT DEFINITE + self.err_init[' *** ERROR *** MATRIX IS NO'] = len(self.err_init) + # *** ERROR *** There are unused relative + self.err_init[' *** ERROR *** There are un'] = len(self.err_init) + # *** ERROR *** Error finding body based + self.err_init[' *** ERROR *** Error findin'] = len(self.err_init) + # *** ERROR *** In body actions + self.err_init[' *** ERROR *** In body acti'] = len(self.err_init) + # *** ERROR *** Command unknown and ignored + self.err_init[' *** ERROR *** Command unkn'] = len(self.err_init) + # *** ERROR *** ERROR - More bodies than elements on main_body: tower + self.err_init[' *** ERROR *** ERROR - More'] = len(self.err_init) + # *** ERROR *** The program will stop + self.err_init[' *** ERROR *** The program '] = len(self.err_init) + # *** ERROR *** Unknown begin command in topologi. + self.err_init[' *** ERROR *** Unknown begi'] = len(self.err_init) + # *** ERROR *** Not all needed topologi main body commands present + self.err_init[' *** ERROR *** Not all need'] = len(self.err_init) + # *** ERROR *** opening timoschenko data file + self.err_init[' *** ERROR *** opening tim'] = len(self.err_init) + # *** ERROR *** Error opening AE data file + self.err_init[' *** ERROR *** Error openin'] = len(self.err_init) + # *** ERROR *** Requested blade _ae set number not found in _ae file + self.err_init[' *** ERROR *** Requested bl'] = len(self.err_init) + # Error opening PC data file + self.err_init[' Error opening PC data file'] = len(self.err_init) + # *** ERROR *** error reading mann turbulence + self.err_init[' *** ERROR *** error readin'] = len(self.err_init) + # *** INFO *** The DLL subroutine + self.err_init[' *** INFO *** The DLL subro'] = len(self.err_init) + # ** WARNING: FROM ESYS ELASTICBAR: No keyword + self.err_init[' ** WARNING: FROM ESYS ELAS'] = len(self.err_init) + # *** ERROR *** DLL ./control/killtrans.dll could not be loaded - error! + self.err_init[' *** ERROR *** DLL'] = len(self.err_init) + # *** ERROR *** The DLL subroutine + self.err_init[' *** ERROR *** The DLL subr'] = len(self.err_init) + # *** ERROR *** Mann turbulence length scale must be larger than zero! + # *** ERROR *** Mann turbulence alpha eps value must be larger than zero! + # *** ERROR *** Mann turbulence gamma value must be larger than zero! + self.err_init[' *** ERROR *** Mann turbule'] = len(self.err_init) + + # *** WARNING *** Shear center x location not in elastic center, set to zero + self.err_init[' *** WARNING *** Shear cent'] = len(self.err_init) + # Turbulence file ./xyz.bin does not exist + self.err_init[' Turbulence file '] = len(self.err_init) + self.err_init[' *** WARNING ***'] = len(self.err_init) + self.err_init[' *** ERROR ***'] = len(self.err_init) + self.err_init[' WARNING'] = len(self.err_init) + self.err_init[' ERROR'] = len(self.err_init) + + # error messages that appear during simulation + self.err_sim = {} + # *** ERROR *** Wind speed requested inside + self.err_sim[' *** ERROR *** Wind speed r'] = len(self.err_sim) + # Maximum iterations exceeded at time step: + self.err_sim[' Maximum iterations exceede'] = len(self.err_sim) + # Solver seems not to converge: + self.err_sim[' Solver seems not to conver'] = len(self.err_sim) + # *** ERROR *** Out of x bounds: + self.err_sim[' *** ERROR *** Out of x bou'] = len(self.err_sim) + # *** ERROR *** Out of limits in user defined shear field - limit value used + self.err_sim[' *** ERROR *** Out of limit'] = len(self.err_sim) + + # TODO: error message from a non existing channel output/input + # add more messages if required... + + self.init_cols = len(self.err_init) + self.sim_cols = len(self.err_sim) + self.header = None + + def readlog(self, fname, case=None, save_iter=False): + """ + """ + # open the current log file + with open(fname, 'r') as f: + lines = f.readlines() + + # keep track of the messages allready found in this file + tempLog = [] + tempLog.append(fname) + exit_correct, found_error = False, False + + subcols_sim = 4 + subcols_init = 2 + # create empty list item for the different messages and line + # number. Include one column for non identified messages + for j in range(self.init_cols): + # 2 sub-columns per message: nr, msg + for k in range(subcols_init): + tempLog.append('') + for j in range(self.sim_cols): + # 4 sub-columns per message: first, last, nr, msg + for k in range(subcols_sim): + tempLog.append('') + # and two more columns at the end for messages of unknown origin + tempLog.append('') + tempLog.append('') + + # if there is a cases object, see how many time steps we expect + if case is not None: + dt = float(case['[dt_sim]']) + time_steps = int(float(case['[time_stop]']) / dt) + iterations = np.ndarray( (time_steps+1,3), dtype=np.float32 ) + else: + iterations = np.ndarray( (len(lines),3), dtype=np.float32 ) + dt = False + iterations[:,0:2] = -1 + iterations[:,2] = 0 + + # keep track of the time_step number + time_step, init_block = -1, True + # check for messages in the current line + # for speed: delete from message watch list if message is found + for j, line in enumerate(lines): + # all id's of errors are 27 characters long + msg = line[:27] + # remove the line terminator, this seems to take 2 characters + # on PY2, but only one in PY3 + line = line.replace('\n', '') + + # keep track of the number of iterations + if line[:12] == ' Global time': + time_step += 1 + iterations[time_step,0] = float(line[14:40]) + # for PY2, new line is 2 characters, for PY3 it is one char + iterations[time_step,1] = int(line[-6:]) + # time step is the first time stamp + if not dt: + dt = float(line[15:40]) + # no need to look for messages if global time is mentioned + continue + + elif line[:20] == ' Starting simulation': + init_block = False + + elif init_block: + # if string is shorter, we just get a shorter string. + # checking presence in dict is faster compared to checking + # the length of the string + # first, last, nr, msg + if msg in self.err_init: + # icol=0 -> fname + icol = subcols_init*self.err_init[msg] + 1 + # 0: number of occurances + if tempLog[icol] == '': + tempLog[icol] = '1' + else: + tempLog[icol] = str(int(tempLog[icol]) + 1) + # 1: the error message itself + tempLog[icol+1] = line + found_error = True + + # find errors that can occur during simulation + elif msg in self.err_sim: + icol = subcols_sim*self.err_sim[msg] + icol += subcols_init*self.init_cols + 1 + # in case stuff already goes wrong on the first time step + if time_step == -1: + time_step = 0 + + # 1: time step of first occurance + if tempLog[icol] == '': + tempLog[icol] = '%i' % time_step + # 2: time step of last occurance + tempLog[icol+1] = '%i' % time_step + # 3: number of occurances + if tempLog[icol+2] == '': + tempLog[icol+2] = '1' + else: + tempLog[icol+2] = str(int(tempLog[icol+2]) + 1) + # 4: the error message itself + tempLog[icol+3] = line + + found_error = True + iterations[time_step,2] = 1 + + # method of last resort, we have no idea what message + elif line[:10] == ' *** ERROR' or line[:10]==' ** WARNING': + icol = subcols_sim*self.sim_cols + icol += subcols_init*self.init_cols + 1 + # line number of the message + tempLog[icol] = j + # and message + tempLog[icol+1] = line + found_error = True + # in case stuff already goes wrong on the first time step + if time_step == -1: + time_step = 0 + iterations[time_step,2] = 1 + + # simulation and simulation output time + if case is not None: + t_stop = float(case['[time_stop]']) + duration = float(case['[duration]']) + else: + t_stop = -1 + duration = -1 + + # see if the last line holds the sim time + if line[:15] == ' Elapsed time :': + exit_correct = True + elapsed_time = float(line[15:-1]) + tempLog.append( elapsed_time ) + # in some cases, Elapsed time is not given, and the last message + # might be: " Closing of external type2 DLL" + elif line[:20] == ' Closing of external': + exit_correct = True + elapsed_time = iterations[time_step,0] + tempLog.append( elapsed_time ) + elif np.allclose(iterations[time_step,0], t_stop): + exit_correct = True + elapsed_time = iterations[time_step,0] + tempLog.append( elapsed_time ) + else: + elapsed_time = -1 + tempLog.append('') + + # give the last recorded time step + tempLog.append('%1.11f' % iterations[time_step,0]) + + # simulation and simulation output time + tempLog.append('%1.01f' % t_stop) + tempLog.append('%1.04f' % (t_stop/elapsed_time)) + tempLog.append('%1.01f' % duration) + + # as last element, add the total number of iterations + itertotal = np.nansum(iterations[:,1]) + tempLog.append('%i' % itertotal) + + # the delta t used for the simulation + if dt: + tempLog.append('%1.7f' % dt) + else: + tempLog.append('failed to find dt') + + # number of time steps + tempLog.append('%i' % len(iterations) ) + + # if the simulation didn't end correctly, the elapsed_time doesn't + # exist. Add the average and maximum nr of iterations per step + # or, if only the structural and eigen analysis is done, we have 0 + try: + ratio = float(elapsed_time)/float(itertotal) + tempLog.append('%1.6f' % ratio) + except (UnboundLocalError, ZeroDivisionError, ValueError) as e: + tempLog.append('') + # when there are no time steps (structural analysis only) + try: + tempLog.append('%1.2f' % iterations[:,1].mean()) + tempLog.append('%1.2f' % iterations[:,1].max()) + except ValueError: + tempLog.append('') + tempLog.append('') + + # save the iterations in the results folder + if save_iter: + fiter = os.path.basename(fname).replace('.log', '.iter') + fmt = ['%12.06f', '%4i', '%4i'] + if case is not None: + fpath = os.path.join(case['[run_dir]'], case['[iter_dir]']) + # in case it has subdirectories + for tt in [3,2,1]: + tmp = os.path.sep.join(fpath.split(os.path.sep)[:-tt]) + if not os.path.exists(tmp): + os.makedirs(tmp) + if not os.path.exists(fpath): + os.makedirs(fpath) + np.savetxt(fpath + fiter, iterations, fmt=fmt) + else: + logpath = os.path.dirname(fname) + np.savetxt(os.path.join(logpath, fiter), iterations, fmt=fmt) + + # append the messages found in the current file to the overview log + self.MsgListLog.append(tempLog) + self.MsgListLog2[fname] = [found_error, exit_correct] + + def _msglistlog2csv(self, contents): + """Write LogFile.MsgListLog to a csv file. Use LogFile._header to + create a header. + """ + for k in self.MsgListLog: + for n in k: + contents = contents + str(n) + ';' + # at the end of each line, new line symbol + contents = contents + '\n' + return contents + + def csv2df(self, fname): + """Read a csv log file analysis and convert to a pandas.DataFrame + """ + colnames, min_itemsize, dtypes = self.headers4df() + df = pd.read_csv(fname, header=0, names=colnames, sep=';', ) + for col, dtype in dtypes.items(): + df[col] = df[col].astype(dtype) + # replace nan with empty for str columns + if dtype == str: + df[col] = df[col].str.replace('nan', '') + return df + + def _header(self): + """Header for log analysis csv file + """ + + # write the results in a file, start with a header + contents = 'file name;' + 'nr;msg;'*(self.init_cols) + contents += 'first_tstep;last_tstep;nr;msg;'*(self.sim_cols) + contents += 'lnr;msg;' + # and add headers for elapsed time, nr of iterations, and sec/iteration + contents += 'Elapsted time;last time step;Simulation time;' + contents += 'real sim time;Sim output time;' + contents += 'total iterations;dt;nr time steps;' + contents += 'seconds/iteration;average iterations/time step;' + contents += 'maximum iterations/time step;\n' + + return contents + + def headers4df(self): + """Create header and a minimum itemsize for string columns when + converting a Log check analysis to a pandas.DataFrame + + Returns + ------- + + header : list + List of column names as generated by WindIO.LogFile._header + + min_itemsize : dict + Dictionary with column names as keys, and the minimum string lenght + as values. + + dtypes : dict + Dictionary with column names as keys, and data types as values + """ + chain_iter = chain.from_iterable + + colnames = ['file_name'] + colnames.extend(list(chain_iter(('nr_%i' % i, 'msg_%i' % i) + for i in range(31))) ) + + gr = ('first_tstep_%i', 'last_step_%i', 'nr_%i', 'msg_%i') + colnames.extend(list(chain_iter( (k % i for k in gr) + for i in range(100,105,1))) ) + colnames.extend(['nr_extra', 'msg_extra']) + colnames.extend(['elapsted_time', + 'last_time_step', + 'simulation_time', + 'real_sim_time', + 'sim_output_time', + 'total_iterations', + 'dt', + 'nr_time_steps', + 'seconds_p_iteration', + 'mean_iters_p_time_step', + 'max_iters_p_time_step', + 'sim_id']) + dtypes = {} + + # str and float datatypes for + msg_cols = ['msg_%i' % i for i in range(30)] + msg_cols.extend(['msg_%i' % i for i in range(100,105,1)]) + msg_cols.append('msg_extra') + dtypes.update({k:str for k in msg_cols}) + # make the message/str columns long enough + min_itemsize = {'msg_%i' % i : 100 for i in range(30)} + + # column names holding the number of occurances of messages + nr_cols = ['nr_%i' % i for i in range(30)] + nr_cols.extend(['nr_%i' % i for i in range(100,105,1)]) + # other float values + nr_cols.extend(['elapsted_time', 'total_iterations']) + # NaN only exists in float arrays, not integers (NumPy limitation) + # so use float instead of int + dtypes.update({k:np.float64 for k in nr_cols}) + + return colnames, min_itemsize, dtypes + + class LoadResults(ReadHawc2): """Read a HAWC2 result data file @@ -817,6 +1223,83 @@ class LoadResults(ReadHawc2): stats['int'] = integrate.trapz(sig[i0:i1, :], x=sig[i0:i1, 0], axis=0) return stats + def statsdel_df(self, i0=0, i1=None, statchans='all', delchans='all', + m=[3, 4, 6, 8, 10, 12], neq=None, no_bins=46): + """Calculate statistics and equivalent loads for the current loaded + signal. + + Parameters + ---------- + + i0 : int, default=0 + + i1 : int, default=None + + channels : list, default='all' + all channels are selected if set to 'all', otherwise define a list + using the unique channel defintions. + + neq : int, default=1 + + no_bins : int, default=46 + + Return + ------ + + statsdel : pd.DataFrame + Pandas DataFrame with the statistical parameters and the different + fatigue coefficients as columns, and channels as rows. As index the + unique channel name is used. + + """ + + stats = ['max', 'min', 'mean', 'std', 'range', 'absmax', 'rms', 'int'] + if statchans == 'all': + statchans = self.ch_df['unique_ch_name'].tolist() + statchis = self.ch_df['unique_ch_name'].index.values + else: + sel = self.ch_df['unique_ch_name'] + statchis = self.ch_df[sel.isin(statchans)].index.values + + if delchans == 'all': + delchans = self.ch_df['unique_ch_name'].tolist() + delchis = self.ch_df.index.values + else: + sel = self.ch_df['unique_ch_name'] + delchis = self.ch_df[sel.isin(delchans)].index.values + + # delchans has to be a subset of statchans! + if len(set(delchans) - set(statchans)) > 0: + raise ValueError('delchans has to be a subset of statchans') + + tmp = np.ndarray((len(statchans), len(stats+m))) + tmp[:,:] = np.nan + m_cols = ['m=%i' % m_ for m_ in m] + statsdel = pd.DataFrame(tmp, columns=stats+m_cols) + statsdel.index = statchans + + datasel = self.sig[i0:i1,statchis] + time = self.sig[i0:i1,0] + statsdel['max'] = datasel.max(axis=0) + statsdel['min'] = datasel.min(axis=0) + statsdel['mean'] = datasel.mean(axis=0) + statsdel['std'] = datasel.std(axis=0) + statsdel['range'] = statsdel['max'] - statsdel['min'] + statsdel['absmax'] = np.abs(datasel).max(axis=0) + statsdel['rms'] = np.sqrt(np.mean(datasel*datasel, axis=0)) + statsdel['int'] = integrate.trapz(datasel, x=time, axis=0) + statsdel['intabs'] = integrate.trapz(np.abs(datasel), x=time, axis=0) + + if neq is None: + neq = self.sig[-1,0] - self.sig[0,0] + + for chi, chan in zip(delchis, delchans): + signal = self.sig[i0:i1,chi] + eq = self.calc_fatigue(signal, no_bins=no_bins, neq=neq, m=m) + statsdel.loc[chan][m_cols] = eq + + return statsdel + # TODO: general signal method, this is not HAWC2 specific, move out def calc_fatigue(self, signal, no_bins=46, m=[3, 4, 6, 8, 10, 12], neq=1): """ @@ -1121,7 +1604,7 @@ class UserWind(object): pass def __call__(self, z_h, r_blade_tip, a_phi=None, shear_exp=None, nr_hor=3, - nr_vert=20, h_ME=500.0, fname=None, wdir=None): + nr_vert=20, h_ME=500.0, io=None, wdir=None): """ Parameters @@ -1147,8 +1630,8 @@ class UserWind(object): Modified Ekman parameter. Take roughly 500 for off shore sites, 1000 for on shore sites. - fname : str, default=None - When specified, the HAWC2 user defined veer input file will be + io : str or io buffer, default=None + When specified, the HAWC2 user defined shear input file will be written. wdir : float, default=None @@ -1158,14 +1641,14 @@ class UserWind(object): Returns ------- - None + uu, vv, ww, xx, zz """ x, z = self.create_coords(z_h, r_blade_tip, nr_vert=nr_vert, nr_hor=nr_hor) if a_phi is not None: - phi_rad = self.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) + phi_rad = WindProfiles.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) assert len(phi_rad) == nr_vert else: nr_vert = len(z) @@ -1174,21 +1657,34 @@ class UserWind(object): if wdir is not None: # because wdir cw positive, and phi veer ccw positive phi_rad -= (wdir*np.pi/180.0) - u, v, w, xx, zz = self.decompose_veer(phi_rad, x, z) - # scale the shear on top of that - if shear_exp is not None: - shear = self.shear_powerlaw(zz, z_h, shear_exp) + u, v, w = self.decompose_veer(phi_rad, nr_hor) + # when no shear is defined + if shear_exp is None: + uu = u + vv = v + ww = w + else: + # scale the shear on top of the veer + shear = WindProfiles.powerlaw(z, z_h, shear_exp) uu = u*shear[:,np.newaxis] vv = v*shear[:,np.newaxis] ww = w*shear[:,np.newaxis] # and write to a file - if fname is not None: - self.write_user_defined_shear(fname, uu, vv, ww, xx, zz) + if isinstance(io, str): + with open(io, 'wb') as fid: + fid = self.write(fid, uu, vv, ww, x, z) + self.fid =None + elif io is not None: + io = self.write(io, uu, vv, ww, x, z) + self.fid = io + + return uu, vv, ww, x, z def create_coords(self, z_h, r_blade_tip, nr_vert=3, nr_hor=20): """ Utility to create the coordinates of the wind field based on hub heigth - and blade length. + and blade length. Add 15% to r_blade_tip to make sure horizontal edges + are defined wide enough. """ # take 15% extra space after the blade tip z = np.linspace(0, z_h + r_blade_tip*1.15, nr_vert) @@ -1197,80 +1693,88 @@ class UserWind(object): return x, z - def shear_powerlaw(self, z, z_ref, a): - profile = np.power(z/z_ref, a) - # when a negative, make sure we return zero and not inf - profile[np.isinf(profile)] = 0.0 - return profile + def deltaphi2aphi(self, d_phi, z_h, r_blade_tip, h_ME=500.0): + """For a given `\\Delta \\varphi` over the rotor diameter, estimate + the corresponding `a_{\\varphi}`. - def veer_ekman_mod(self, z, z_h, h_ME=500.0, a_phi=0.5): - """ - Modified Ekman veer profile, as defined by Mark C. Kelly in email on - 10 October 2014 15:10 (RE: veer profile) + Parameters + ---------- - .. math:: - \\varphi(z) - \\varphi(z_H) \\approx a_{\\varphi} - e^{-\sqrt{z_H/h_{ME}}} - \\frac{z-z_H}{\sqrt{z_H*h_{ME}}} - \\left( 1 - \\frac{z-z_H}{2 \sqrt{z_H h_{ME}}} - - \\frac{z-z_H}{4z_H} \\right) + `\\Delta \\varphi` : ndarray or float + Veer angle difference over the rotor plane from lowest to highest + blade tip position. - where: - :math:`h_{ME} \\equiv \\frac{\\kappa u_*}{f}` - and :math:`f = 2 \Omega \sin \\varphi` is the coriolis parameter, - and :math:`\\kappa = 0.41` as the von Karman constant, - and :math:`u_\\star = \\sqrt{\\frac{\\tau_w}{\\rho}}` friction velocity. + z_h : float + Hub height in meters. - For on shore, :math:`h_{ME} \\approx 1000`, for off-shore, - :math:`h_{ME} \\approx 500` + r_blade_tip : float + Blade tip radius/length. - :math:`a_{\\varphi} \\approx 0.5` + h_ME : float, default=500.0 + Modified Ekman parameter. For on shore, + :math:`h_{ME} \\approx 1000`, for off-shore, + :math:`h_{ME} \\approx 500` + + Returns + ------- + + `a_{\\varphi}` : ndarray or float + + """ + + t1 = r_blade_tip * 2.0 * np.exp(-z_h/(h_ME)) + a_phi = d_phi * np.sqrt(h_ME*z_h) / t1 + return a_phi + + def deltaphi2aphi_opt(self, deltaphi, z, z_h, r_blade_tip, h_ME): + """ + convert delta_phi over a given interval z to a_phi using + scipy.optimize.fsolve on veer_ekman_mod. Parameters ---------- - :math:`a_{\\varphi} \\approx 0.5` parameter for the modified - Ekman veer distribution. Values vary between -1.2 and 0.5. - - returns - ------- + deltaphi : float + Desired delta phi in rad over interval z[0] at bottom to z[1] at + the top. - phi_rad : ndarray - veer angle in radians """ - t1 = np.exp(-math.sqrt(z_h / h_ME)) - t2 = (z - z_h) / math.sqrt(z_h * h_ME) - t3 = (1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h)) + def func(a_phi, z, z_h, h_ME, deltaphi_target): + phis = WindProfiles.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) + return np.abs(deltaphi_target - (phis[1] - phis[0])) - return a_phi * t1 * t2 * t3 + args = (z, z_h, h_ME, deltaphi) + return sp.optimize.fsolve(func, [0], args=args)[0] - def decompose_veer(self, phi_rad, x, z): + def decompose_veer(self, phi_rad, nr_hor): """ Convert a veer angle into u, v, and w components, ready for the - HAWC2 user defined veer input file. + HAWC2 user defined veer input file. nr_vert refers to the number of + vertical grid points. Paramters --------- - phi_rad : ndarray - veer angle in radians + phi_rad : ndarray(nr_vert) + veer angle in radians as function of height - method : str, default=linear - 'linear' for a linear veer, 'ekman_mod' for modified ekman method + nr_hor : int + Number of horizontal grid points Returns ------- - u, v, w, v_coord, w_coord + u : ndarray(nr_hor, nr_vert) - """ + v : ndarray(nr_hor, nr_vert) + + w : ndarray(nr_hor, nr_vert) - nr_hor = len(x) - nr_vert = len(z) - assert len(phi_rad) == nr_vert + """ + nr_vert = len(phi_rad) tan_phi = np.tan(phi_rad) # convert veer angles to veer components in v, u. Make sure the @@ -1295,11 +1799,11 @@ class UserWind(object): v_full = v[:, np.newaxis] + np.zeros((3,))[np.newaxis, :] w_full = np.zeros((nr_vert, nr_hor)) - return u_full, v_full, w_full, x, z + return u_full, v_full, w_full - def load_user_defined_veer(self, fname): + def read(self, fname): """ - Load a user defined veer and shear file as used for HAWC2 + Read a user defined shear input file as used for HAWC2. Returns ------- @@ -1336,9 +1840,9 @@ class UserWind(object): return u_comp, v_comp, w_comp, v_coord, w_coord, phi_deg - def write_user_defined_shear(self, fname, u, v, w, v_coord, w_coord, - fmt_uvw='% 08.05f', fmt_coord='% 8.02f'): - """ + def write(self, fid, u, v, w, v_coord, w_coord, fmt_uvw='% 08.05f', + fmt_coord='% 8.02f'): + """Write a user defined shear input file for HAWC2. """ nr_hor = len(v_coord) nr_vert = len(w_coord) @@ -1353,40 +1857,38 @@ class UserWind(object): 'nr_hor and nr_vert: u.shape: %s, nr_hor: %i, ' 'nr_vert: %i' % (str(u.shape), nr_hor, nr_vert)) - # and create the input file - with open(fname, 'wb') as fid: - fid.write(b'# User defined shear file\n') - fid.write(b'%i %i # nr_hor (v), nr_vert (w)\n' % (nr_hor, nr_vert)) - h1 = b'normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns' - fid.write(b'# v component, %s\n' % h1) - np.savetxt(fid, v, fmt=fmt_uvw, delimiter=' ') - fid.write(b'# u component, %s\n' % h1) - np.savetxt(fid, u, fmt=fmt_uvw, delimiter=' ') - fid.write(b'# w component, %s\n' % h1) - np.savetxt(fid, w, fmt=fmt_uvw, delimiter=' ') - h2 = b'# v coordinates (along the horizontal, nr_hor, 0 rotor center)' - fid.write(b'%s\n' % h2) - np.savetxt(fid, v_coord.reshape((v_coord.size, 1)), fmt=fmt_coord) - h3 = b'# w coordinates (zero is at ground level, height, nr_hor)' - fid.write(b'%s\n' % h3) - np.savetxt(fid, w_coord.reshape((w_coord.size, 1)), fmt=fmt_coord) + fid.write(b'# User defined shear file\n') + tmp = '%i %i # nr_hor (v), nr_vert (w)\n' % (nr_hor, nr_vert) + fid.write(tmp.encode()) + h1 = 'normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns' + fid.write(('# v component, %s\n' % h1).encode()) + np.savetxt(fid, v, fmt=fmt_uvw, delimiter=' ') + fid.write(('# u component, %s\n' % h1).encode()) + np.savetxt(fid, u, fmt=fmt_uvw, delimiter=' ') + fid.write(('# w component, %s\n' % h1).encode()) + np.savetxt(fid, w, fmt=fmt_uvw, delimiter=' ') + h2 = '# v coordinates (along the horizontal, nr_hor, 0 rotor center)' + fid.write(('%s\n' % h2).encode()) + np.savetxt(fid, v_coord.reshape((v_coord.size, 1)), fmt=fmt_coord) + h3 = '# w coordinates (zero is at ground level, height, nr_hor)' + fid.write(('%s\n' % h3).encode()) + np.savetxt(fid, w_coord.reshape((w_coord.size, 1)), fmt=fmt_coord) + + return fid class WindProfiles(object): - def __init__(self): - pass - - def logarithmic(self, z, z_ref, r_0): + def logarithmic(z, z_ref, r_0): return np.log10(z/r_0)/np.log10(z_ref/r_0) - def powerlaw(self, z, z_ref, a): + def powerlaw(z, z_ref, a): profile = np.power(z/z_ref, a) # when a negative, make sure we return zero and not inf profile[np.isinf(profile)] = 0.0 return profile - def veer_ekman_mod(self, z, z_h, h_ME=500.0, a_phi=0.5): + def veer_ekman_mod(z, z_h, h_ME=500.0, a_phi=0.5): """ Modified Ekman veer profile, as defined by Mark C. Kelly in email on 10 October 2014 15:10 (RE: veer profile) @@ -1412,20 +1914,29 @@ class WindProfiles(object): Parameters ---------- - :math:`a_{\\varphi} \\approx 0.5` parameter for the modified - Ekman veer distribution. Values vary between -1.2 and 0.5. + z : ndarray(n) + z-coordinates (height) of the grid on which the veer angle should + be calculated. - returns + z_h : float + Hub height in meters. + + :math:`a_{\\varphi}` : default=0.5 + Parameter for the modified Ekman veer distribution. Value varies + between -1.2 and 0.5. + + Returns ------- phi_rad : ndarray - veer angle in radians as function of height + Veer angle in radians as function of z. + """ t1 = np.exp(-math.sqrt(z_h / h_ME)) t2 = (z - z_h) / math.sqrt(z_h * h_ME) - t3 = ( 1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h) ) + t3 = (1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h)) return a_phi * t1 * t2 * t3