├── task19iceloss.pdf ├── images ├── power_curve_example.PNG └── task19_banner_a_text.png ├── t19_ice_loss ├── __init__.py └── data_file_handler.py ├── docs ├── source │ ├── t19.rst │ ├── index.rst │ ├── license.rst │ ├── disclaimer.rst │ ├── conf.py │ ├── background.rst │ └── usage.rst ├── Makefile └── make.bat ├── setup.py ├── DISCLAIMER.txt ├── LICENSE ├── CHANGELOG ├── multifile_t19_counter.py ├── example.ini ├── README.md └── t19_counter.py /task19iceloss.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEAWind-Task19/T19IceLossMethod/HEAD/task19iceloss.pdf -------------------------------------------------------------------------------- /images/power_curve_example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEAWind-Task19/T19IceLossMethod/HEAD/images/power_curve_example.PNG -------------------------------------------------------------------------------- /images/task19_banner_a_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEAWind-Task19/T19IceLossMethod/HEAD/images/task19_banner_a_text.png -------------------------------------------------------------------------------- /t19_ice_loss/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_file_handler import CSVimporter 2 | from .data_file_handler import Result_file_writer 3 | from .aep_counter import AEPcounter 4 | -------------------------------------------------------------------------------- /docs/source/t19.rst: -------------------------------------------------------------------------------- 1 | ###################################################### 2 | Class Documentation for the Task 19 power loss counter 3 | ###################################################### 4 | 5 | .. automodule:: t19_ice_loss 6 | 7 | .. autoclass:: AEPcounter 8 | :members: 9 | 10 | .. autoclass:: CSVimporter 11 | :members: 12 | 13 | .. autoclass:: Result_file_writer 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Task 19 Ice Loss documentation master file, created by 2 | sphinx-quickstart on Wed Jun 12 15:52:35 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Task 19 Ice Loss's documentation! 7 | ============================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | :caption: Contents: 12 | 13 | disclaimer 14 | 15 | license 16 | 17 | background 18 | 19 | usage 20 | 21 | t19 22 | 23 | 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="t19_ice_loss", # Replace with your own username 8 | version="2.2.2", 9 | author="IEA Wind Task 19", 10 | author_email="timo.karlsson@vtt.fi", 11 | description="A tool to estimate icing losses from wind turbine SCADA data", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/IEAWind-Task19/IceLossMethod", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: BSD License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.6', 22 | ) -------------------------------------------------------------------------------- /DISCLAIMER.txt: -------------------------------------------------------------------------------- 1 | The Software is provided "AS IS" and VTT makes no representations or 2 | warranties of any kind with respect to the Software or proprietary 3 | rights, whether express or implied, including, but not limited to 4 | merchantability, fitness for a particular purpose and non-infringement 5 | of third party rights such as copyrights, trade secrets or any patent. 6 | VTT shall have no liability whatsoever for the use of the Software or any 7 | output obtained through the use of the Software by you. 8 | 9 | The Software relies on following third-party products, that are distributed 10 | with licenses linked below: 11 | 12 | Python: 13 | https://docs.python.org/3/license.html 14 | NumPy: 15 | http://www.numpy.org/license.html 16 | SciPy: 17 | https://www.scipy.org/scipylib/license.html 18 | Matplotlib: 19 | http://matplotlib.org/users/license.html 20 | 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | License 3 | ####### 4 | 5 | Copyright 2019 IEA Wind Task 19 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, IEA Wind Task 19 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG: 2 | 3 | v 3.0: 2019- xxx 4 | 5 | * Added a stop code to the code 6 | * Stop code can be used to highlight icing caused stops if there is a separate status code in scada for that. 7 | * Fixed datatype issues with newr versions of numpy 8 | * Added IPS handling capability to loss counter 9 | * Added Power curve uncertainty to power curves: 10 | * uncertainty defined as std_dev/mean 11 | * Added Status code based stops as a separate case for output 12 | * Allows counting production losses during a certain status code 13 | * Added IPS and status code stops to summary file 14 | * Added power curve uncertainty to summary file and printed power curve 15 | * Time Based Availability and Energy Based Availability 16 | * Icing losses now counted against actual production instead of theoretical reference 17 | 18 | 19 | 20 | v 2.0: 2017-04-04 21 | 22 | * Changed power curve from mean to P50 23 | * fixed bug in air density correction 24 | * fixed bug in production statistics 25 | * separated the temperature level filters 26 | * different filter levels for building the reference dataset and ice detection 27 | * reformatted the summary summary 28 | * added an example input file that works 29 | * included example case results in the release package 30 | 31 | v. 1.1: 2015-11-27 11:25 32 | 33 | * fixed a crash with zero output 34 | * Added new filtering options 35 | * adjustable power level filter 36 | * adjustable temperature level filter 37 | * custom start and stop times 38 | * fixed issues with time stamp indexing 39 | * no longer assumes timestamps to be at column 0 40 | 41 | v. 1.0 42 | 43 | Initial public release 44 | 45 | -------------------------------------------------------------------------------- /docs/source/disclaimer.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | DISCLAIMER 3 | ########## 4 | 5 | IEA Wind 6 | The International Energy Agency Implementing Agreement for Co-operation in the Research, Development and Deployment of Wind Energy Systems (IEA Wind) is a vehicle for member countries to exchange information on the planning and execution of national, large-scale wind system projects and to undertake co-operative research and development projects called Tasks or Annexes. IEA Wind is part of IEA’s Technology Collaboration Programme or TCP. 7 | 8 | The IEA Wind agreement, also known as the Implementing Agreement for Co-operation in the Research, Development, and Deployment of Wind Energy Systems, functions within a framework created by the International Energy Agency (IEA). Views, findings, and publications of IEA Wind do not necessarily represent the views or policies of the IEA Secretariat or of all its individual member countries. 9 | 10 | Task 19 11 | For the wind industry, cold climate refers to sites that may experience significant periods of icing events, temperatures below the operational limits of standard wind turbines, or both. There is vast potential for producing electricity at these often windy and uninhabited cold climate sites. Consequently, the International Energy Agency Wind Agreement has since 2002, operated the international working group Task 19 Wind Energy in Cold Climates. The goal of this cooperation is to gather and disseminate information about wind energy in cold climates and to establish guidelines and state-of-the-art information. 12 | 13 | 14 | The Software is provided "AS IS" and VTT makes no representations or 15 | warranties of any kind with respect to the Software or proprietary 16 | rights, whether express or implied, including, but not limited to 17 | merchantability, fitness for a particular purpose and non-infringement 18 | of third party rights such as copyrights, trade secrets or any patent. 19 | VTT shall have no liability whatsoever for the use of the Software or any 20 | output obtained through the use of the Software by you. 21 | 22 | The software requires using following third-party products. The licenses of these products can be found from links below. 23 | 24 | Python: 25 | https://docs.python.org/3/license.html 26 | 27 | 28 | NumPy: 29 | http://www.numpy.org/license.html 30 | 31 | 32 | SciPy: 33 | https://www.scipy.org/scipylib/license.html 34 | 35 | 36 | Matplotlib: 37 | http://matplotlib.org/users/license.html 38 | 39 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.append(os.path.join(os.path.dirname(__name__), '../')) 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'Task 19 Ice Loss' 20 | copyright = '2019, IEA Wind Task 19' 21 | author = 'IEA Wind Task 19' 22 | 23 | # The full version, including alpha/beta/rc tags 24 | release = '2.2' 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.githubpages', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # List of patterns, relative to source directory, that match files and 42 | # directories to ignore when looking for source files. 43 | # This pattern also affects html_static_path and html_extra_path. 44 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 45 | 46 | source_suffix = '.rst' 47 | master_doc = 'index' 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ['_static'] 56 | 57 | # Custom sidebar templates, must be a dictionary that maps document names 58 | # to template names. 59 | # 60 | # The default sidebars (for documents that don't match any pattern) are 61 | # defined by theme itself. Builtin themes are using these templates by 62 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 63 | # 'searchbox.html']``. 64 | # 65 | # html_sidebars = {} 66 | html_theme = 'classic' 67 | 68 | -------------------------------------------------------------------------------- /multifile_t19_counter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import t19_counter 3 | import collections 4 | import fileinput 5 | import configparser 6 | from multiprocessing import Pool 7 | 8 | 9 | def find_value_by_tag(filename, option): 10 | """ 11 | find singular value from a summary file defined by parameter option 12 | 13 | option is exact copy of what reads on the wanted line in the summary file 14 | """ 15 | not_found = True 16 | final_value = '' 17 | with open(filename, 'r') as result_file: 18 | while not_found: 19 | try: 20 | line = next(result_file) 21 | tokens = line.split('\t') 22 | tag = tokens[0].strip() 23 | value = tokens[1].strip() 24 | if tag == option: 25 | not_found = False 26 | final_value = value 27 | break 28 | except StopIteration: 29 | final_value = '' 30 | break 31 | return final_value 32 | 33 | 34 | def parse_summary_file_into_dict(filename): 35 | """ 36 | reads everyline of the summary file into a dictionary 37 | """ 38 | done = False 39 | # results = {} 40 | results = collections.OrderedDict() 41 | with open(filename, 'r') as result_file: 42 | while not done: 43 | try: 44 | line = next(result_file) 45 | tokens = line.split('\t') 46 | tag = tokens[0].strip() 47 | value = tokens[1].strip() 48 | if tag != ' ' or tag != '': 49 | results[tag] = value 50 | else: 51 | break 52 | except StopIteration: 53 | break 54 | return results 55 | 56 | 57 | def clean_results_for_printing(results): 58 | """ 59 | creates a column for final output file 60 | """ 61 | output = [] 62 | for k, v in results.items(): 63 | if k != 'Field': 64 | output.append(v) 65 | return output 66 | 67 | 68 | def get_keys_for_printing(results): 69 | output = [] 70 | for k in results.keys(): 71 | if k != 'Field': 72 | output.append(k) 73 | return output 74 | 75 | 76 | def results_to_file(filename, results, separator='\t'): 77 | keys = get_keys_for_printing(results[0]) 78 | with open(filename, 'w') as outfile: 79 | for key in keys: 80 | line = key 81 | for summary in results: 82 | line += separator 83 | line += summary[key] 84 | line += '\n' 85 | outfile.write(line) 86 | 87 | 88 | 89 | def combined_summary(result_directory): 90 | results = [] 91 | for f in [result_directory + f for f in os.listdir(result_directory) if 'summary.txt' in f]: 92 | results.append(parse_summary_file_into_dict(f)) 93 | output_filename = os.path.join(result_directory,'_combined_summary.csv') 94 | results_to_file(output_filename,results) 95 | 96 | 97 | def main(): 98 | # directory containing all .ini files for individual turbines 99 | source_directory = './data/siteconfigs/' 100 | 101 | # result_directory, needs to be defined in .ini files as well 102 | result_directory = './results/' 103 | 104 | #get list of .ini files 105 | 106 | turbines = [source_directory + filename for filename in os.listdir(source_directory) if ('.ini' in filename) and ('blank') not in filename] 107 | 108 | ## run the script sequentially 109 | # for turb in turbines: 110 | # t19_counter.main(turb) 111 | ## use all 4 cores 112 | with Pool(4) as p: 113 | p.map(t19_counter.main,turbines) 114 | 115 | # combine summaryfiles into one large set 116 | combined_summary(result_directory) 117 | 118 | 119 | 120 | if __name__ == '__main__': 121 | main() 122 | -------------------------------------------------------------------------------- /example.ini: -------------------------------------------------------------------------------- 1 | [Source file] 2 | # name of the dataset, used to identify result files 3 | id = ExampleDataset 4 | # can also be a relative path 5 | filename = ./fake_data2.csv 6 | # field delimiter in source file, if tabulator delimited, write TAB 7 | delimiter = , 8 | # quote character used to mark text fields, if no quote char, write NONE 9 | quotechar = NONE 10 | # datetime format as per python datetime 11 | datetime format = %d.%m.%Y %H:%M 12 | # extra characters at the end of timestamps 13 | datetime extra char = 0 14 | # skip these columns when reading 15 | skip columns = NONE 16 | # datafile columns that store the fault and status codes separated with a comma 17 | # include all columns with non-numeric values here 18 | fault columns = 5,6,7,8 19 | # if fault/status codes are in the file as text this needs to be set to True 20 | replace fault codes = True 21 | 22 | 23 | [Output] 24 | # directory where the result files will be put. 25 | result directory = ./results/example/ 26 | # write a summary. txt file 27 | summary = True 28 | # draw the power curve plot, write it to .png file 29 | plot = True 30 | # write a time series of icing alarms 31 | alarm time series = True 32 | # write the time series data into a file with some useful added columns 33 | filtered raw data = True 34 | # Write icing event start stop times into a file 35 | icing events = True 36 | # write the power curve into a .txt file 37 | power curve = True 38 | 39 | # set options for used data structure. Column indexing starts from 0 40 | [Data Structure] 41 | timestamp index = 0 42 | wind speed index = 1 43 | wind direction index = 2 44 | temperature index = 3 45 | power index = 4 46 | # in units used in the datafile 47 | rated power = 2000 48 | state index = 5 49 | normal state = OK 50 | # meters above sea level 51 | site elevation = 100 52 | status index = 6 53 | # statuscode value that indicates that the turbine is stopped 54 | status code stop value = STOP 55 | 56 | # icing related options, indexing starts from 0 57 | [Icing] 58 | # is there an ice detection signal in the SCADA, ice detector or other signal 59 | ice detection = True 60 | # signal value that indicates icing alarm 61 | icing alarm code = WHAT 62 | icing alarm index = 7 63 | # Is there heating, set to False if not 64 | heating = True 65 | # ips status code that indicates that blade heating is on 66 | ips status code = ON 67 | ips status index = 8 68 | # see documentation on this, defines the way ips status is defined 69 | ips status type = 1 70 | # set the value to negative if there is no explicit heating power measurement 71 | ips power consumption index = -1 72 | 73 | 74 | # set binning options 75 | [Binning] 76 | # smallest wind speed bin. All wind speeds below this value end up in the first bin 77 | minimum wind speed = 0 78 | # largest wind speed bin, all values above this will end up in the last bin 79 | maximum wind speed = 30 80 | #bin width in m/s 81 | wind speed bin size = 0.5 82 | # bin size for directional binning in degrees 83 | wind direction bin size = 360 84 | 85 | # filtering options 86 | [Filtering] 87 | # lower limit for power curve aka P10 88 | power drop limit = 10 89 | # power curve upper limit 90 | overproduction limit = 90 91 | # icing time filter (number of samples) 92 | icing time = 3 93 | # limit for defining the stop limit 94 | stop limit multiplier = 0.005 95 | # time filter length for stop detection (in samples once again) 96 | stop time filter = 6 97 | #state filter type; see documentation for details 98 | statefilter type = 1 99 | # how the status code indicating stop should be interpreted, see the documentation for details 100 | stop filter type = 1 101 | # power level filter to remove obvious stopppages from data 102 | power level filter = 0.01 103 | # temperature limit to create the reference dataset in degrees Celsius 104 | reference temperature = 3 105 | # temeperature limit for icing event 106 | temperature filter = 1 107 | # minimum bin size 108 | min bin size = 36 109 | # additional filter for power curve smoothing 110 | distance filter = True 111 | # start and stop times for time limiting the analysis 112 | start time = NONE 113 | stop time = NONE 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Task19 Logo](images/task19_banner_a_text.png) 2 | 3 | # Task19 Ice Loss Method 4 | 5 | A standardized method to assess production losses due to icing from wind turbine SCADA data. This site describes a method to assess production losses due to icing based on standard SCADA data available from modern wind turbines. 6 | 7 | # Motivation 8 | 9 | Production losses due to wind turbine rotor icing are calculated with different methods, all resulting in different results. Task 19 has three main reasons why a standardized production loss calculation method is needed: 10 | 11 | * There is a large need to compare different sites with each other with a systematic analysis method 12 | * To validate the IEA Ice Classification 13 | * Evaluate effectiveness of various blade heating systems 14 | 15 | With the method described here, anyone with access to SCADA data from wind turbines can assess and calculate turbine specific production losses due to icing. The standardized method will use existing standards and will be developed in order to minimize the uncertainties related to production loss estimations from SCADA data. The method does not require icing measurements as input. 16 | 17 | # Method 18 | 19 | Task 19 proposes a method that is robust, easily adaptable, filters outliers automatically and does not assume a normal distribution of the SCADA data for individual turbines and wind farms. The proposed method uses percentiles of the reference, non-iced power curve in combination with temperature measurements. Ice build-up on turbine blades gradually deteriorates the power output (or results to overproduction to iced anemometer) so for increased accuracy the method uses three consecutive 10-minute data points for defining start-stop timestamps for icing events. In other words, the turbine rotor is used as an ice detector. Iced turbine power losses are defined by comparing the performance to the calculated power curve using heated anemometers from nacelle and the measured reference, expected power curve. Production losses are separated into 2 categories: operation and standstill losses due to icing. The different icing event cases are illustrated in the picture below: Event class A, reduced production due to icing, Event Class B, turbine stops due to icing and event class C, apparent overproduction due to icing. 20 | ![Iceing event examples](images/power_curve_example.PNG) 21 | 22 | On a general level, the method can be divided into 3 main steps: 23 | 24 | 1. Calculate reference, non-iced power curve 25 | 2. Calculate start-stop timestamps for different icing event classes 26 | 3. Calculate production losses due to icing 27 | 28 | Below is the list of required SCADA data signals used as input for the production loss calculation method. 29 | 30 | Signal | Description | Unit | Value 31 | ------ | ----------- | ---- | ----- 32 | ws | Hub height wind speed | m/s | 10-minute mean 33 | temp | Ambient temperature (hub height) | °C | 10-minute mean 34 | pwr mean | Turbine output power | kW | 10-minute mean 35 | mode | Turbine operational mode | - | 10-minute mean 36 | 37 | # Usage 38 | 39 | The production loss calculator is configured by setting up a config file. Then calling the script ``t19_counter.py`` by giving the ini file as a command line parameter as : 40 | 41 | python t19_counter.py site.ini 42 | 43 | where ``site.ini`` contains the case definition relevant for your site. See the included documentation for more details on how to set up the .ini file. 44 | 45 | 46 | 47 | # IEA Wind 48 | The International Energy Agency Implementing Agreement for Co-operation 49 | in the Research, Development and Deployment of Wind Energy Systems (IEA Wind) is 50 | a vehicle for member countries to exchange information on the planning and execution 51 | of national, large-scale wind system projects and to undertake co-operative research and 52 | development projects called Tasks or Annexes. IEA Wind is part of IEA’s Technology 53 | Collaboration Programme or TCP. 54 | 55 | # Task 19 56 | For the wind industry, cold climate refers to sites that may experience significant 57 | periods of icing events, temperatures below the operational limits of standard wind 58 | turbines, or both. There is vast potential for producing electricity at these often windy 59 | and uninhabited cold climate sites. Consequently, the International Energy Agency 60 | Wind Agreement has since 2002, operated the international working group Task 19 61 | Wind Energy in Cold Climates. The goal of this cooperation is to gather and 62 | disseminate information about wind energy in cold climates and to establish guidelines 63 | and state-of-the-art information. 64 | 65 | # Disclaimer: 66 | The IEA Wind agreement, also known as the Implementing Agreement for 67 | Co-operation in the Research, Development, and Deployment of Wind Energy Systems, 68 | functions within a framework created by the International Energy Agency (IEA). Views, 69 | findings, and publications of IEA Wind do not necessarily represent the views or policies 70 | of the IEA Secretariat or of all its individual member countries. 71 | 72 | # License 73 | 74 | The code is available under the Three Clause BSD License. 75 | 76 | -------------------------------------------------------------------------------- /t19_counter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from t19_ice_loss import aep_counter as aep 3 | from t19_ice_loss import data_file_handler as dfh 4 | import sys 5 | import configparser 6 | import datetime as dt 7 | import numpy as np 8 | 9 | 10 | 11 | 12 | def main(configfile_name): 13 | """ 14 | Process the data and write the outputfiles. 15 | 16 | Outputfiles are named based on the dataset id. 17 | 18 | """ 19 | # # # get the configfile as a command-line parameter 20 | #configfile_name = sys.argv[1] 21 | # first read the configfile in 22 | config = configparser.ConfigParser() 23 | config.read(configfile_name) 24 | print("{0} : Processing dataset {1}".format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), config.get('Source file', 'id'))) 25 | 26 | # first read the data in 27 | reader = dfh.CSVimporter() 28 | # read generic settings from the ini file 29 | # configfile_name = 'test.ini' 30 | # configfile_name = 'C11.ini' 31 | 32 | 33 | reader.read_file_options_from_file(configfile_name) 34 | if not os.path.exists(reader.result_dir): 35 | os.makedirs(reader.result_dir) 36 | #reader.fault_columns = [8,9,10] 37 | #reader.replace_faults = True 38 | # set filename separately 39 | #reader.filename = '../data/full_mean_dataset.csv' 40 | #read data 41 | reader.read_data() 42 | data = reader.full_data 43 | headers = reader.headers 44 | 45 | # print(headers) 46 | # create a new instance of the AEP loss counter 47 | aepc = aep.AEPcounter() 48 | 49 | # TODO: 50 | # figure out a netter way for this. 51 | if reader.replace_faults: 52 | aepc.fault_dict = reader.fault_dict 53 | # read data structure from file 54 | aepc.set_data_options_from_file(configfile_name) 55 | # read binning options from file 56 | aepc.set_binning_options_from_file(configfile_name) 57 | # read filter settings from file 58 | aepc.set_filtering_options_from_file(configfile_name) 59 | # read options related to IPS, if the "Icing" section does not exist, set IPS to False 60 | aepc.set_ips_options_from_file(configfile_name) 61 | # aepc.starttimestamp = dt.datetime(2015, 1, 1, 0, 0, 0) 62 | # aepc.stoptimestamp = dt.datetime(2015, 10, 1, 0, 0, 0) 63 | # calculate air density correction based on site height using the formula from the spec 64 | temperature_corrected_data = aepc.air_density_correction(data) 65 | # temperature_corrected_data = data.copy() 66 | # filter the corrected data based on state variable values 67 | #state_filtered_data = aepc.state_filter_data(temperature_corrected_data) 68 | time_limited_data = aepc.time_filter_data(temperature_corrected_data) 69 | state_filtered_data = aepc.state_filter_data(time_limited_data) 70 | 71 | # filter the data based on power level, 72 | # remove datapoints where output power is below 0.01 * aepc.rated_power 73 | #power_level_filtered_data = aepc.power_level_filter(state_filtered_data, 0.01) 74 | power_level_filtered_data = aepc.power_level_filter(state_filtered_data) 75 | # create power curves. This bins the data according to wind speed and direction and does some 76 | # filtering and interpolation to fill over gaps on source data. 77 | 78 | # only use the part of data where temperature is above 3 degrees celsius for the power curve 79 | # use the full dataset for refernce use time limited for loss calculation 80 | s_reference_data = aepc.state_filter_data(temperature_corrected_data) 81 | d_reference_data = aepc.temperature_filter_data(s_reference_data) 82 | reference_data = aepc.power_level_filter(d_reference_data) 83 | #reference_data = aepc.diff_filter(pd_reference_data) 84 | pc = aepc.count_power_curves(reference_data) 85 | # rfw.write_power_curve_file('../results/power_curve.txt', pc, aepc) 86 | # save data sizes into a list in order, original, filtered, reference 87 | data_sizes = [len(data), len(state_filtered_data), len(reference_data)] 88 | 89 | # find stoppages as defined in the specification 90 | # find power drops and flag them 91 | if aepc.stop_filter_type == 0: 92 | stops = aepc.find_icing_related_stops(state_filtered_data, pc) 93 | pow_alms1 = aepc.power_alarms(power_level_filtered_data, pc) 94 | status_timings = None 95 | status_stops = None 96 | elif (aepc.stop_filter_type == 2) or (aepc.stop_filter_type == 1): 97 | status_stops = aepc.status_code_stops(time_limited_data, pc) 98 | stops = aepc.find_icing_related_stops(state_filtered_data, pc) 99 | pow_alms1 = aepc.power_alarms(power_level_filtered_data, pc) 100 | status_timings = aepc.power_loss_during_alarm(status_stops) 101 | else: 102 | stops = None 103 | status_stops = None 104 | status_timings = None 105 | pow_alms1 = aepc.power_alarms(power_level_filtered_data, pc) 106 | if aepc.heated_site: 107 | ips_on_flags = aepc.status_code_stops(time_limited_data, pc, filter_type='ips') 108 | ips_timings = aepc.power_loss_during_alarm(ips_on_flags, ips_alarm=True) 109 | else: 110 | ips_on_flags = None 111 | ips_timings = None 112 | if aepc.ice_detection: 113 | ice_detected = aepc.status_code_stops(time_limited_data, pc, filter_type='icing') 114 | ice_timings = aepc.power_loss_during_alarm(ice_detected) 115 | else: 116 | ice_detected = None 117 | ice_timings = None 118 | # find over production incidents and flag them 119 | pow_alms2 = aepc.power_alarms(power_level_filtered_data, pc, over=True) 120 | # find start and stop times of alarms in the structure containing the power drop flags 121 | alarm_timings = aepc.power_loss_during_alarm(pow_alms1) 122 | stop_timings = aepc.power_loss_during_alarm(stops) 123 | over_timings = aepc.power_loss_during_alarm(pow_alms2) 124 | 125 | # re-do the reference dataset 126 | #new_ref = aepc.increase_reference_dataset(time_limited_data, stop_timings, alarm_timings, over_timings) 127 | #new_pc = aepc.count_power_curves(new_ref) 128 | 129 | rfw = dfh.Result_file_writer() 130 | rfw.set_output_file_options(configfile_name) 131 | 132 | if rfw.summaryfile_write: 133 | summary_status, summary_filename, summary_error = rfw.summary_statistics(aepc, time_limited_data, reference_data, pc, alarm_timings, stop_timings, over_timings, status_timings, ice_timings, ips_timings, data_sizes) 134 | if summary_status: 135 | print("{0} : Summary written successfully into: {1}".format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), summary_filename)) 136 | else: 137 | print("{0} : Problem writing summary: {1}".format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), summary_error)) 138 | 139 | if rfw.power_curve_write: 140 | power_curve_status, pc_filename, pc_error = rfw.write_power_curve(aepc,pc) 141 | if power_curve_status: 142 | print('{0} : Power curve written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), pc_filename)) 143 | else: 144 | print('{0} : Problem writing power curve: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), pc_error)) 145 | 146 | if rfw.icing_events_write: 147 | # TODO: write these to one file 148 | prod_loss_trunk = '_losses.csv' 149 | stops_trunk = '_stops.csv' 150 | status_trunk = '_status.csv' 151 | ips_trunk = '_ips.csv' 152 | icing_trunk = '_ice_det.csv' 153 | losses_filename = aepc.result_dir + aepc.id + prod_loss_trunk 154 | stops_filename = aepc.result_dir + aepc.id + stops_trunk 155 | status_filename = aepc.result_dir + aepc.id + status_trunk 156 | ips_filename = aepc.result_dir + aepc.id + ips_trunk 157 | icing_filename = aepc.result_dir + aepc.id + icing_trunk 158 | loss_status, loss_write_error = rfw.write_alarm_timings(losses_filename, alarm_timings) 159 | if loss_status: 160 | print('{0} : Icing loss statistics written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), losses_filename)) 161 | else: 162 | print('{0} : Error writing icing loss statistics: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), loss_write_error)) 163 | stop_status, stop_write_error = rfw.write_alarm_timings(stops_filename, stop_timings) 164 | if stop_status: 165 | print('{0} : Icing stops statistics written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), stops_filename)) 166 | else: 167 | print('{0} : Error writing icing stop statistics: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), stop_write_error)) 168 | if aepc.status_stop_index[0] > 0: 169 | status_status, status_write_error = rfw.write_alarm_timings(status_filename, status_timings) 170 | if status_status: 171 | print('{0} : Status Code statistics written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), status_filename)) 172 | else: 173 | print('{0} : Error writing Status Code statistics: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), status_write_error)) 174 | if aepc.heated_site: 175 | ips_status, ips_write_error = rfw.write_alarm_timings(ips_filename, ips_timings) 176 | if ips_status: 177 | print('{0} : Status Code statistics written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ips_filename)) 178 | else: 179 | print('{0} : Error writing Status Code statistics: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ips_write_error)) 180 | if aepc.ice_detection: 181 | icing_status, icing_write_error = rfw.write_alarm_timings(icing_filename, ice_timings) 182 | if icing_status: 183 | print('{0} : Status Code statistics written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), icing_filename)) 184 | else: 185 | print('{0} : Error writing Status Code statistics: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), icing_write_error)) 186 | 187 | #TODO: make ice detector and IPS OPTIONAL, Now the code inserts dummy values for IPS. Not a clean solution 188 | monthly_stat_status, stat_filename, stat_write_error = rfw.write_monthly_stats(time_limited_data, pc, aepc, pow_alms1, stops, status_stops, ips_on_flags, ice_detected) 189 | if monthly_stat_status: 190 | print('{0} : Monthly icing loss timeseries written into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), stat_filename)) 191 | else: 192 | print('{0} : Error writing loss timeseries: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), stat_write_error)) 193 | 194 | if rfw.alarm_time_series_file_write: 195 | # write out the results 196 | combined_ts = aepc.combine_timeseries(pow_alms1,stops,pow_alms2) 197 | alarm_timeseries_filename = aepc.result_dir + aepc.id + '_alarms.csv' 198 | ts_write_status, ts_write_error = rfw.write_alarm_file(alarm_timeseries_filename, combined_ts) 199 | if ts_write_status: 200 | print('{0} : Time series written successfully into: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), alarm_timeseries_filename)) 201 | else: 202 | print('{0} : Error writing time series file: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ts_write_error)) 203 | 204 | if rfw.filtered_raw_data_write: 205 | filtered_data_filename = aepc.result_dir + aepc.id + '_filtered.csv' 206 | new_data = rfw.insert_fault_codes(aepc.time_filter_data(temperature_corrected_data), aepc, reader) 207 | raw_write_status, raw_write_error = rfw.write_time_series_file(filtered_data_filename, new_data, headers,aepc,pc) 208 | if raw_write_status: 209 | print('{0} : Filtered data written succesfully to: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),filtered_data_filename)) 210 | else: 211 | print('{0} : Error writeing raw data: {1}'.format(dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),raw_write_error)) 212 | 213 | if rfw.pc_plot_picture: 214 | if aepc.heated_site: 215 | rfw.generate_standard_plots(temperature_corrected_data, pc, aepc, pow_alms1, pow_alms2, stops, data_sizes, 216 | alarm_timings, over_timings, stop_timings, ips_on_flags, True) 217 | else: 218 | rfw.generate_standard_plots(temperature_corrected_data, pc, aepc, pow_alms1, pow_alms2, stops, data_sizes, 219 | alarm_timings, over_timings, stop_timings, None, True) 220 | 221 | 222 | 223 | if __name__ == '__main__': 224 | main(sys.argv[1]) 225 | -------------------------------------------------------------------------------- /docs/source/background.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | Task 19 Ice Loss Method 3 | ####################### 4 | 5 | =========== 6 | Description 7 | =========== 8 | 9 | This document describes a method to assess production losses due to icing based on standard SCADA data available from modern wind turbines. An open source Python code will be publically available based on the method presented in this guideline document. This method is formulated by `IEA Task 19 `_ , an international expert group with an aim to increase and disseminate knowledge and information about cold climate wind energy related issues. Task 19 aims to contribute in lowering the costs and reducing the risks of wind energy deployment in cold climates. 10 | 11 | ========== 12 | Motivation 13 | ========== 14 | 15 | 16 | Currently production losses due to wind turbine rotor icing are calculated with different methods all resulting to different results. Task 19 has three main reasons on why a standardized production loss calculation method is needed: 17 | 18 | 1. There is a large need to compare different sites with each other with a systematic analysis method 19 | 2. To validate the IEA Ice Classification 20 | 3. Evaluate effectiveness of various blade heating systems 21 | 22 | Current production loss methods usually use a constant -15% or -25% clean power curve drop as an indication of icing. Similarly standard deviation (or multiples of it) has been widely used to define iced turbine production losses. Both of these methods result to different results and are not necessarily representing the actual ice build-up and removal process to wind turbine blades reliably enough. 23 | 24 | With the method described here, anyone with access to SCADA data from wind turbines can assess and calculate turbine specific production losses due to icing. The method uses existing standards and is developed in order to minimize the uncertainties related to production loss estimations from SCADA data. The method does not require icing measurements as input. 25 | 26 | ====== 27 | Method 28 | ====== 29 | 30 | Task 19 proposes a method that is robust, easily adaptable, filters outliers automatically and does not assume a normal distribution of the SCADA data for individual turbines and wind farms. The proposed method uses percentiles of the reference, non-iced power curve in combination with temperature measurements. Ice build-up on turbine blades gradually deteriorates the power output (or results to overproduction to to iced anemometer) so for increased accuracy the method uses three consecutive 10-minute data points for defining start-stop timestamps for icing events. In other words, the turbine rotor is used as an ice detector. Iced turbine power losses are defined by comparing the performance to the calculated power curve using heated anemometers from nacelle and the measured reference, expected power curve. Production losses are separated into 2 categories: operation and standstill losses due to icing. 31 | 32 | On a general level, the method can be divided into 3 main steps: 33 | 34 | 1. Calculate reference, non-iced power curve 35 | 2. Calculate start-stop timestamps for different icing event classes 36 | 3. Calculate production losses due to icing 37 | 38 | Below is a minimal list of signals used to calculate the icing losses 39 | 40 | 41 | +-----------+----------------------------+-------+----------------+ 42 | | Signal | Description | Unit | Value | 43 | +===========+============================+=======+================+ 44 | | ws | Hub height wind speed | m/s | 10-minute mean | 45 | +-----------+----------------------------+-------+----------------+ 46 | | temp | Ambient temperature | °C | 10-minute mean | 47 | | | (hub height) | | | 48 | +-----------+----------------------------+-------+----------------+ 49 | | pwr mean | Turbine output power | kW | 10-minute mean | 50 | +-----------+----------------------------+-------+----------------+ 51 | | mode | Turbine operational mode | | 10-minute mean | 52 | +-----------+----------------------------+-------+----------------+ 53 | 54 | ================================================= 55 | Step 1: Calculate reference, non-iced power curve 56 | ================================================= 57 | 58 | This is the first and very important step in defining the production losses due to icing as one always needs to compare iced rotor performance to reference, non-iced operational values. All iced turbine production losses (operational or stand-still related) will be compared to what the turbine could produce during (and after) icing events. 59 | 60 | Air density is to be corrected to hub height according to ISO 2533 by scaling the wind speed and air pressure by taking site elevation above sea level. As air pressure measurements are typically missing from turbine SCADA, a static pressure according to site elevation above sea level is calculated [#f2]_. Site air density and air pressure are used to calibrate the nacelle wind speed as follows: 61 | 62 | .. math:: 63 | 64 | w_{site} = w_{std} \times \left ( \frac{\rho_{std}}{\rho_{site}} \right )^{\frac{1}{3}} = w_{std} \times \left ( \frac{\frac{P_{std}}{T_{std}}}{\frac{P_{site}}{T_{site}}} \right )^{\frac{1}{3}} \\ 65 | w_{site} = w_{std} \times \left ( \frac{T_{std}}{T_{site}}(1-2.25577 \times 10^{-5} \times h)^{5.25588} \right )^{\frac{1}{3}} 66 | 67 | where w\ :sub:`site` is calibrated nacelle wind speed, w\ :sub:`std` measured nacelle wind speed, T\ :sub:`site` is nacelle 68 | temperature [#f3]_ and T\ :sub:`std` is standard temperature of 15°C (288.15 K) resulting to air density of 1.225 69 | kg/m3 at sea level P\ :sub:`std` = 101325 Pa ambient air pressure, h is site elevation in meters above sea 70 | level [#f4]_. 71 | 72 | The IEC 61400-12-1 “Power performance measurements” is applicable for very detailed power production calculations using a standard met mast wind measurements as input. However, the method for production loss calculation using SCADA data only results to using nacelle top wind measurements which are disturbed by the rotating rotor. The nacelle anemometer is thus less accurate and simplified binning of the reference data is proposed. As a first step, reference turbine data needs to be temperature and air pressure corrected and filtered according to production mode [#f5]_ as follows: 73 | 74 | * air density and static air pressure correction with nacelle temperature and site elevation 75 | * power production operating states only AND temperature [#f6]_ > +3°C 76 | 77 | For power curve calculation the data is binned according to wind speed and optionally according to wind direction as well. Usefulness of wind direction-based binning depends on the site geography and is not always necessary. A separate power curve is then calculated for each wind direction bin. 78 | 79 | The power curve calculation results in several for each wind speed bin: 80 | 81 | 1. Median output power in the bin 82 | 2. Standard deviation of power in the bin 83 | 3. 10th percentile of power in the bin 84 | 4. 90th percentile of power in the bin 85 | 5. Power curve uncertainty in the bin defined as [standard deviation] / [power] 86 | 6. Sample count in the bin 87 | 88 | Sample count can be used to determine the appropriate binning resolution, it is recommended to have at least 6 hours of data in a bin to get a representative result. 89 | 90 | The code will also try to interpolate over empty bins or bins that have too few samples in them. In these cases a linear interpolation between two closest bins is used. 91 | 92 | 93 | 94 | ========================================================================= 95 | Step 2: Calculate start-stop timestamps for different icing event classes 96 | ========================================================================= 97 | 98 | Once the reference has been established, next the exact time periods when ice is present on the turbine rotor are needed. As only SCADA data is used as input to define icing events, special care needs to take place in order to minimize false icing event alarms. False iced rotor alarms are minimized by assuming that ice is affecting the rotor for 30 minutes or more consecutively at below 0°C temperatures. The required output power reduction (or over production) uses a certain percentile of the reference data. This enables a robust yet simple threshold. 99 | 100 | In total, there are three different icing event classes detected from the SCADA data: 101 | 102 | 1: Decreased production ,icing event class a), shortly IEa 103 | 2: Standstill icing event class b), IEb 104 | 3: Iced up heated anemometer ws or overproduction icing event class c), IEc) 105 | 106 | In addition to these, if blade heating system is available, the moments when blade heating is on can be categorized separately and if ice detector is available, icing events detected by the ice detector can be categorized separately. 107 | 108 | 109 | 110 | -------------------- 111 | Icing event class a) 112 | -------------------- 113 | 114 | The start of a typical reduced power output icing event class a) [IEa] for an operational turbine is 115 | defined as follows: 116 | 117 | If temp is below 0°C AND power is below 10th percentile of the respective reference (non-iced) wind bin for 30 minutes or more, THEN icing event class a) starts 118 | 119 | An icing event class a) ends as follows: 120 | 121 | If power is above 10th percentile of the respective reference wind bin for 30-min or more, THEN icing event class a) ends 122 | 123 | In the output files icing event class a is referenced as `Production losses due to icing` 124 | 125 | -------------------- 126 | Icing event class b) 127 | -------------------- 128 | 129 | Icing can cause the turbine to shut-down and cause the turbine to standstill for a number of reasons. 130 | Standstill due to icing caused by icing event class b) [IEb] begins as follows: 131 | 132 | If temp is below 0°C AND power is below 10th percentile of the respective reference wind bin for 10-min resulting to a shutdown (power < 0.5 % of rated power of the turbine for at least 20-min, THEN standstill due to icing starts 133 | 134 | Icing event b) ends as follows: 135 | 136 | If power is above 10th percentile of the respective reference wind bin for 30-min or more, THEN icing event class b) ends 137 | 138 | ------------------------------------------- 139 | Manual analysis of shut-downs in wintertime 140 | ------------------------------------------- 141 | 142 | Sometimes the turbine controller shuts down the turbine due to safety reasons during iced turbine operation even before power P10-P90 thresholds are exceeded. Different turbine types react very differently to icing of the rotor during operation. Some turbine types are very sensitive to rotor icing and thus shut-down very quickly after icing influences the rotor. Other turbine models are extremely robust and are able to operate with iced blades for long periods even under severe icing conditions. Manual analysis of standstill losses is recommended because standstill losses are typically larger than operational losses and analysing operational losses only underestimates production losses due to icing. 143 | 144 | Typical shut -down controller error messages report excess tower side-to-side vibrations or that the nacelle wind speed does not correspond to output power. This type of behaviour can be considered to be caused by icing and is to be manually added when summing up all production losses. 145 | 146 | It is possible to define certain SCADA status codes to represent a stopped turbine and calculate the losses caused by these stops separately from all other production losses. This can be useful in some cases to understand the distribution of production losses into different categories. 147 | 148 | -------------------- 149 | Icing event class c) 150 | -------------------- 151 | 152 | The heated anemometer ws may sometimes be influenced by ice resulting to overproduction. 153 | The start of an overproduction (iced up anemometer) icing event class c) [IEc] for an operational turbine 154 | is as follows: 155 | 156 | If temp is below 0°C AND power is above 90th percentile of the respective reference wind bin for 30-min or more, THEN icing event class c) starts 157 | 158 | Icing event class c) ends as follows: 159 | 160 | If power is below 90th percentile of the respective reference wind bin for 30-min or more, THEN icing event class c) ends 161 | 162 | For IEa and IEb, the production losses can be defined. However, if the measured output power is above expected wind speed (ie overproducing) in IEc, there is reason to expect the anemometer is influenced by ice and for this case, the production losses cannot be defined unless accurate wind speed are available from another source. If the number of hours with IEc is large, the estimated total production losses can be considered as minimum losses because all icing influences cannot be assessed. 163 | 164 | ------------------------------------------------ 165 | Step 3: Calculate production losses due to icing 166 | ------------------------------------------------ 167 | 168 | Once the icing events have been identified the difference in power between the reference and actual measured output power will be calulated for each time step during the icing events. In addition to this a production losses in kWh and as a percentage of total ar calculated for ice event classes IEa and IEb. For overproduction (class IEc) only the total duration is documented. 169 | 170 | The output of the method and the formatting of the results are described in the usage section of the documentation 171 | 172 | 173 | .. [#f2] Alternatively, detailed weather model air pressure values can used. Of course if air pressure is measured, that is the preferred alternative 174 | .. [#f3] Warning: Some nacelle temperature sensors have shown a constant bias of +2...3 °C due to radiation heat of nacelle. Investigating this bias is recommended (compare to met mast, weather models etc) 175 | .. [#f4] Engineering ToolBox, (2003). Altitude above Sea Level and Air Pressure. [online] Available at: https://www.engineeringtoolbox.com/air-altitude-pressure-d_462.html 176 | .. [#f5] Alternatively, if controller mode is not available or known, use following filter criterias: IF P\ :sub:`min` > 0.005 P\ :sub:`rated` AND P\ :sub:`mean` > 0.05 P\ :sub:`rated` THEN Power production mode = normal 177 | .. [#f6] This temperature limit needs to be set high enough to assume that turbine is not influenced by icing at these temperatures 178 | 179 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | #################################### 2 | Using the Task 19 Power Loss Counter 3 | #################################### 4 | 5 | The project contains sample code to calculate icing induced data losses for a single wind turbine according to the specification by IEA Wind Task 19. 6 | 7 | ************ 8 | Installation 9 | ************ 10 | 11 | Code is written in Python 3 and it uses several libraries included in the Scipy stack. 12 | 13 | Needed libraries are: :: 14 | 15 | numpy, scipy, matplotlib, (sphinx) 16 | 17 | Information on installing the scipy stack can be found from `the Scipy website `_ 18 | 19 | Sphinx is needed to build the documentation. It's not mandatory, the release package includes a compiled documentation. 20 | 21 | Note that you will need python 3 versions of the libraries. 22 | 23 | Easiest way to get everything is to use a prepackaged installer such as `Anaconda `_ 24 | 25 | Anaconda installer contains all the required Python libraries and more and is provided as one easy to use installer. It is free and open source, available for Windows/Mac OS X/Linux and is self contained i.e. can be installed side by side with an existing python installation. 26 | 27 | .. _use: 28 | 29 | ************** 30 | Using the code 31 | ************** 32 | 33 | The code can be used to calculate losses and it will output several kinds of statistics about your data for later analysis. 34 | 35 | The production loss calculator is configured by setting up a config file. (see section The .ini file). Then calling the script ``t19_counter.py`` by giving the ini file as a command line parameter as :: 36 | 37 | python t19_counter.py site.ini 38 | 39 | where ``site.ini`` contains the case definition relevant for your site. 40 | 41 | ********** 42 | Input data 43 | ********** 44 | 45 | The code is meant to calculate losses from one time series at a time. One time series in this case means one wind turbine. For multiple turbines you need to define a separate .ini file for each wind turbine and calculate losses separately. After this you have to use other tools to combine individual turbines to each other. 46 | 47 | .. _input-data-example: 48 | 49 | ================== 50 | Example input data 51 | ================== 52 | 53 | Below is an example of what input data could look like. Notice the Status and fault code columns on the right. In this case there needs to be additional filtering to replace the status and fault codes with numerical values. 54 | 55 | ================= =============== ======== =============== ========= ============ ========= ============= 56 | Time stamp (0) Temperature (1) RPM (2) Wind Speed (3) Power(4) Direction(5) Status(6) Fault code(7) 57 | ================= =============== ======== =============== ========= ============ ========= ============= 58 | 2013-02-01 17:20 -3.14 10.2 7.5988 1277.235 133 Run OK 59 | 2013-02-01 17:30 -3.80 10.8 7.6623 1235.741 132 Run OK 60 | 2013-02-01 17:40 -3.23 10.9 7.5914 1297.725 134 Run OK 61 | 2013-02-01 17:50 -3.57 10.5 7.9407 1227.176 130 Run OK 62 | 2013-02-01 18:00 -3.79 10.9 7.8154 1256.481 132 Run OK 63 | 2013-02-01 18:10 -3.73 10.9 7.6261 1274.133 132 Run OK 64 | 2013-02-01 18:20 -3.63 10.8 7.3955 1249.529 136 Run OK 65 | 2013-02-01 18:30 -3.87 10.9 7.691 1232.532 137 Run OK 66 | 2013-02-01 18:40 -3.29 10.3 7.7816 1270.953 135 Run OK 67 | 2013-02-01 18:50 -3.52 10.6 7.9739 1299.535 135 Run OK 68 | 2013-02-01 19:00 -3.15 10.9 7.3878 1221.514 131 Run OK 69 | 2013-02-01 19:10 -3.34 10.9 7.8072 1256.669 131 Run OK 70 | 2013-02-01 19:20 -3.15 10.8 7.7349 1284.479 134 Run OK 71 | 2013-02-01 19:30 -3.08 10.3 7.8621 1288.962 135 Run OK 72 | 2013-02-01 19:40 -3.13 10.7 7.4672 1230.259 133 Run OK 73 | 2013-02-01 19:50 -3.48 10.9 7.509 1279.426 138 Run OK 74 | 2013-02-01 20:00 -3.34 10.5 7.9378 1239.045 139 Run OK 75 | 2013-02-01 20:10 -3.02 10.3 7.1774 1273.976 132 Run OK 76 | 2013-02-01 20:20 -3.50 10.5 7.3004 1254.343 136 Run OK 77 | 2013-02-01 20:30 -3.47 10.7 7.7331 1278.701 131 Run OK 78 | 2013-02-01 20:40 -3.53 10.7 7.6289 1269.522 134 Run OK 79 | 2013-02-01 20:50 -3.04 10.1 7.0893 1296.482 135 Run OK 80 | 2013-02-01 21:00 -3.31 10.7 7.8652 1278.775 133 Run OK 81 | 2013-02-01 21:10 -3.44 10.7 7.6277 1232.615 134 Run OK 82 | 2013-02-01 21:20 -3.46 10.5 7.9821 1219.4 135 Run OK 83 | 2013-02-01 21:30 -3.80 10.7 7.5614 1280.438 132 Run OK 84 | 2013-02-01 21:40 -3.26 10.7 7.2718 1253.659 136 Run OK 85 | 2013-02-01 21:50 -3.75 10.5 7.6549 0 137 Fault Fault code A 86 | 2013-02-01 22:00 -3.15 10.8 7.6856 0 133 Fault Fault code A 87 | 2013-02-01 22:10 -3.89 10.7 7.8238 0 135 Fault Fault code A 88 | 2013-02-01 22:20 -3.80 10.4 7.1408 0 133 Fault Fault code A 89 | 2013-02-01 22:30 -3.86 10.4 7.1721 0 133 Fault Fault code A 90 | 2013-02-01 22:40 -3.04 10.1 7.6194 0 136 Fault Fault code A 91 | ================= =============== ======== =============== ========= ============ ========= ============= 92 | 93 | 94 | 95 | ******* 96 | Outputs 97 | ******* 98 | 99 | There are multiple different outputs available. 100 | 101 | ============ 102 | Summary file 103 | ============ 104 | 105 | Summary file that contains some statistics about the data. A useful tool to get an overview of the data and some statistics 106 | 107 | Contains the following information. 108 | 109 | .. tabularcolumns:: |\Y{0.2}|\Y{0.8}| 110 | 111 | +------------------------------------------------+---------------------------------------------------------------------+ 112 | |Value Field name | Purpose | 113 | +================================================+=====================================================================+ 114 | |Dataset name | Data set name as defined in the config file | 115 | +------------------------------------------------+---------------------------------------------------------------------+ 116 | |Production losses due to icing | Production losses during operation, that are classified to be | 117 | | | icing related, in kWh | 118 | +------------------------------------------------+---------------------------------------------------------------------+ 119 | |Relative production losses due to icing | Previous line's losses as % of reference | 120 | +------------------------------------------------+---------------------------------------------------------------------+ 121 | |Losses due to icing related stops | Losses due to stops during operation that are classified to be | 122 | | | icing related | 123 | +------------------------------------------------+---------------------------------------------------------------------+ 124 | |Relative losses due to icing related stops | Previous line's losses as % of reference | 125 | +------------------------------------------------+---------------------------------------------------------------------+ 126 | |Icing during production | Icing time in hours during production. | 127 | | | Same definition of icing as on row 2 | 128 | +------------------------------------------------+---------------------------------------------------------------------+ 129 | |Icing during production (% of total data) | Previous line's value as % of the entire dataset | 130 | +------------------------------------------------+---------------------------------------------------------------------+ 131 | |Turbine stopped during production | Amount of time turbine is stopped due to icing. Same definition | 132 | | | of stop as "icing related stops" above | 133 | +------------------------------------------------+---------------------------------------------------------------------+ 134 | |Turbine stopped production (% of total data) | Previous line's value as % of the entire dataset | 135 | +------------------------------------------------+---------------------------------------------------------------------+ 136 | |Over production hours | Amount of time in hours the production is above P90 curve | 137 | | | and temperature is below the alarm limit | 138 | +------------------------------------------------+---------------------------------------------------------------------+ 139 | |Over production hours (% of total) | Previous line's value as % of the entire dataset | 140 | +------------------------------------------------+---------------------------------------------------------------------+ 141 | |IPS on hours | Number of hours blade heating is on. | 142 | | | (Will only appear in summary if the site in question has IPS) | 143 | +------------------------------------------------+---------------------------------------------------------------------+ 144 | |IPS on hours (% of total) | Previous line's value as % of the entire dataset | 145 | +------------------------------------------------+---------------------------------------------------------------------+ 146 | |Losses during IPS operation | Sum of production losses during the times IPS is operating. | 147 | | | The loss here is difference between reference and actual value, | 148 | | | IPS self consumption is not taken into account. | 149 | | | (Will only appear in summary if the site in question has IPS). | 150 | +------------------------------------------------+---------------------------------------------------------------------+ 151 | |Relative losses during IPS operation | Previous line's losses as % of reference | 152 | +------------------------------------------------+---------------------------------------------------------------------+ 153 | |IPS self consumption | If there is an IPS power consumption value in the source data, | 154 | | | IPS self consumption in kWh, will show up here | 155 | +------------------------------------------------+---------------------------------------------------------------------+ 156 | |IPS self consumption (% of total) | Previous line's losses as % of reference | 157 | +------------------------------------------------+---------------------------------------------------------------------+ 158 | |SCADA forced stops | Number of hours the turbine is stopped due to some reason | 159 | | | as indicated by the SCADA status code | 160 | +------------------------------------------------+---------------------------------------------------------------------+ 161 | |Time Based Availability (TBA) | Percentage of the time the turbine is operating normally | 162 | +------------------------------------------------+---------------------------------------------------------------------+ 163 | |Loss during SCADA stops | Production loss during the times turbine is not operating in kWh | 164 | +------------------------------------------------+---------------------------------------------------------------------+ 165 | |Relative losses during SCADA stops (% of total) | Previous line's losses as % of reference | 166 | +------------------------------------------------+---------------------------------------------------------------------+ 167 | |Power curve uncertainty | Average of power curve uncertainty | 168 | | | (calculated only for bins between 4 m/s and 15 m/s) | 169 | +------------------------------------------------+---------------------------------------------------------------------+ 170 | |Production upper limit (std.dev) | Upper limit for the production using power curve uncertainty above | 171 | +------------------------------------------------+---------------------------------------------------------------------+ 172 | |Production lower limit (std.dev) | Lower limit for the production using power curve uncertainty above | 173 | +------------------------------------------------+---------------------------------------------------------------------+ 174 | |Production P90 | Production estimate using the P90 power curve | 175 | +------------------------------------------------+---------------------------------------------------------------------+ 176 | |Production P10 | Production estimate using the P10 power curve | 177 | +------------------------------------------------+---------------------------------------------------------------------+ 178 | |Theoretical mean production | Production assuming the reference power curve, | 179 | | | using the wind speed measurement in file, | 180 | | | not taking turbine state into account | 181 | +------------------------------------------------+---------------------------------------------------------------------+ 182 | |Observed power production | Total production calculated from the output power column | 183 | +------------------------------------------------+---------------------------------------------------------------------+ 184 | |Total Losses | Observed power - Theoretical mean power | 185 | +------------------------------------------------+---------------------------------------------------------------------+ 186 | |Energy Based Availability (EBA) | Observed Power / Theoretical mean power as % | 187 | +------------------------------------------------+---------------------------------------------------------------------+ 188 | |Data start time | First time stamp used for analysis | 189 | +------------------------------------------------+---------------------------------------------------------------------+ 190 | |Data stop time | Last time stamp used for analysis | 191 | +------------------------------------------------+---------------------------------------------------------------------+ 192 | |Total amount of data | difference between start and stop time in hours | 193 | +------------------------------------------------+---------------------------------------------------------------------+ 194 | |Reference data start time | First time stamp in data | 195 | +------------------------------------------------+---------------------------------------------------------------------+ 196 | |Reference data stop time | Last time stamp in data | 197 | +------------------------------------------------+---------------------------------------------------------------------+ 198 | |Total amount of data in reference dataset | difference between start and stop time in reference data hours | 199 | +------------------------------------------------+---------------------------------------------------------------------+ 200 | |Data availability | % of data available between first and last timestamp | 201 | +------------------------------------------------+---------------------------------------------------------------------+ 202 | |Sample count in original data | Sample count in the dataset that is read in at first stage | 203 | +------------------------------------------------+---------------------------------------------------------------------+ 204 | |Sample count in after filtering | Sample count after all filtering steps | 205 | +------------------------------------------------+---------------------------------------------------------------------+ 206 | |Data loss due to filtering | Amount of data lost during filtering | 207 | +------------------------------------------------+---------------------------------------------------------------------+ 208 | |Sample count in reference data | Sample count in reference data, | 209 | | | used to build the reference power curve | 210 | +------------------------------------------------+---------------------------------------------------------------------+ 211 | |Reference dataset as % of original data | reference dataset size as % of original | 212 | +------------------------------------------------+---------------------------------------------------------------------+ 213 | 214 | 215 | ================ 216 | data time series 217 | ================ 218 | 219 | Prints a time series data as a .csv file that can be used for further analysis. Data is formatted as columns 220 | 221 | timestamp, alarm, wind speed, reference power, temperature, power, limit 222 | 223 | Here **alarm** indicates possible icing events. Alarm codes in this data are 224 | 225 | 0. no alarm 226 | 1. icing during production. Reduced power output 227 | 2. Turbine stopped due to icing 228 | 3. Overproduction. The turbine output is above the power curve. 229 | 230 | **reference power** is power calculated from the power curve. Limit is the P10 limit used to identify reduced power output. Timestamp, wind speed and output power are drawn from the source data. 231 | 232 | =========== 233 | Power curve 234 | =========== 235 | 236 | Produces one file, that contains individual power curves for each wind direction bin. 237 | 238 | The power curve is output as a table in a text file where different wind speed bins are in each row of the table and different columns indicate different wind direction bins. The row and column headers contain the center points of all bins. 239 | 240 | The file contains the following variables binned for wind speed and direction: 241 | 242 | * Mean power in the bin 243 | * P10 value of the bin 244 | * P90 value of the bin 245 | * Bin power standard deviation 246 | * Power curve uncertainty in the bin 247 | * Power curve upper and lower limits (mean power +- uncertainty) 248 | * Sample count in the bin 249 | 250 | ==== 251 | plot 252 | ==== 253 | 254 | Creates two interactive plots that can be used to look at the data. One contains full time series of the data with icing events marked on the timeline. Other contains the power curve and a scatter plot of the full time series with icing events marked on the data. 255 | 256 | 257 | ================ 258 | icing event list 259 | ================ 260 | 261 | It is possible to output a collected summary of icing events. This is output into two separate files. One that contains a list of all cases where the power output was reduced according to the set conditions and a another one listing all the icing induced stops. both files are text .csv files that containing the fields: 262 | 263 | ========= ======== ======== ============ 264 | starttime stoptime loss_sum event_length 265 | ========= ======== ======== ============ 266 | 267 | Here ``loss_sum`` is the total losses during the event in kilowatt hours and ``event_length`` is the total length of said individual event in hours. 268 | 269 | ==================== 270 | filtered time series 271 | ==================== 272 | 273 | Produces the raw time series that is used after initial filtering to perform all calculations. Can be used for further analysis to get a common starting point. 274 | 275 | 276 | 277 | 278 | ************* 279 | The .ini file 280 | ************* 281 | 282 | All configuration is done in the .ini file. 283 | 284 | Options are denoted in the file as:: 285 | 286 | name of option = value 287 | 288 | File is divided into sections, section headers are enclosed in square brackets \[\]. 289 | 290 | Capitalization of sections and options is important, they need to be spelled the same way as in the example file. 291 | 292 | Not all options are needed. Some variables have a preset default value that does not need to be set. A minimal .inifile is included with the release 293 | 294 | 295 | 296 | ******************** 297 | Config file sections 298 | ******************** 299 | 300 | 301 | The file is divided into Five logical sections that set certain parameters that will change from site to site and between runs. 302 | 303 | Contents of each section are listed below and the purpose of all options is explained briefly. 304 | 305 | 306 | ==================== 307 | Section: Source File 308 | ==================== 309 | 310 | -- 311 | id 312 | -- 313 | 314 | Identifier for the data set. This can be for example the name of the site or a combinations of site name and turbine identifier. **id** is used for example in naming the output files. **id** needs to be unique, if output files with the same identifier exist in the result directory the script will overwrite them. 315 | **id** is a mandatory value. 316 | 317 | 318 | -------- 319 | filename 320 | -------- 321 | 322 | the source data filename and path. The source data needs to be in a ``.csv`` file. Or any other kind of text file. 323 | 324 | --------- 325 | delimiter 326 | --------- 327 | 328 | field delimiter in the source file. If data is tab-delimited write ``TAB`` here. Default value is ``,``. 329 | 330 | --------- 331 | quotechar 332 | --------- 333 | 334 | Character used to indicate text fields in the source file. If no special quote character is used write ``none``. ``none`` is also the default. 335 | 336 | .. _datetime-format: 337 | 338 | --------------- 339 | datetime format 340 | --------------- 341 | 342 | Formatting of timestamps. Uses same notation as Python ``datetime`` class function. See documentation at python.org `here `_ 343 | 344 | Example: timestamp ``2019-09-13 16:09:10`` corresponds to format string ``%Y-%m-%d %H:%M:%S`` 345 | 346 | Defaults to ISO 8601 format ``%Y-%m-%d %H:%M:%S`` 347 | 348 | 349 | ------------------- 350 | datetime extra char 351 | ------------------- 352 | 353 | number of extra character at the end of the timestamp. Sometimes there are some characters add to timestamps e.g. to indicate timezone. The numbers of these need to be defined even if zero. Default value is 0. 354 | 355 | ------------- 356 | fault columns 357 | ------------- 358 | 359 | data file columns that contain the turbine status or fault code. **Zero based** i.e. leftmost column in source file is column 0. If information about the turbine state is contained in multiple places add all of these columns here separated by commas e.g.:: 360 | 361 | fault columns = 8,9,10 362 | 363 | In the :ref:`input-data-example` you would put 6 and 7 here. Because both of those columns can then be used to filter the data based on status information. 364 | 365 | This is mandatory value 366 | 367 | ------------------- 368 | replace fault codes 369 | ------------------- 370 | 371 | filtering option needed in case the source file contains status/fault codes that are not numbers. Non-numeric data in the data set cause issues for the analysis code, so the fault codes need to filtered first. In case the fault/status codes in the source data are text, set:: 372 | 373 | replace fault codes = True 374 | 375 | if the replacement is not needed set this to ``False``. In the example earlier :ref:`input-data-example`. This filtering is needed. in some cases the output fault codes are already numeric, so in those cases it can be false. 376 | 377 | Defaults to ``False`` 378 | 379 | =============== 380 | Section: Output 381 | =============== 382 | 383 | This section defines the output produced by the power loss counter script. 384 | 385 | The script allows the user to set what kind of outputs are needed. All data is output into text files in a results directory. All output files are named us the `id` identifier. 386 | 387 | If a certain output is needed set the value of the corresponding key to ``True`` 388 | 389 | For example producing the alarm time series is relatively slow. Setting unneeded parts to ``False`` can make calculations faster. 390 | 391 | By default all outputs are set to ``True`` and the results are written to the local directory of the script. 392 | 393 | ---------------- 394 | result directory 395 | ---------------- 396 | 397 | directory where the results will be written to 398 | 399 | 400 | ------- 401 | summary 402 | ------- 403 | 404 | Prints a summary statistics file containing overall information about the original data. 405 | 406 | ---------------- 407 | data time series 408 | ---------------- 409 | 410 | sets time series saving on or off. **NOTE** constructing the time series can take a long time depending on the size of the data set. When doing preliminary analysis, unless absolutely required, it is recommended to keep this set as False 411 | 412 | ----------- 413 | power curve 414 | ----------- 415 | 416 | Prints a file that contains the power curve calculated from the data. 417 | 418 | ---- 419 | plot 420 | ---- 421 | 422 | sets plotting on or off. Script makes a power curve plot with icing events highlighted. The plots are saved in to the results directory as ``.png`` 423 | 424 | ---------------- 425 | icing event list 426 | ---------------- 427 | 428 | set the icing event list saving on or off 429 | 430 | ----------------- 431 | filtered raw data 432 | ----------------- 433 | 434 | switch the raw data saving on or off 435 | 436 | ----------------- 437 | Alarm time series 438 | ----------------- 439 | 440 | Print a time series file of the icing alarms. The file will be a .csv file with the following columns: 441 | 442 | ========= ================== ========== =============== =========== ===== ================= 443 | Timestamp Alarm signal value Wind Speed Reference Power Temperature Power Power limit (P10) 444 | ========= ================== ========== =============== =========== ===== ================= 445 | 446 | Here ``Alarm signal value`` indicates the icing status. Values of the alarm signal are listed in the table below 447 | 448 | ================== ============== 449 | Alarm signal value Interpretation 450 | ================== ============== 451 | 0 No alarm 452 | 1 Icing alarm, reduced production 453 | 2 Icing alarm, stop during operation 454 | 3 Overproduction 455 | ================== ============== 456 | 457 | ======================= 458 | Section: Data Structure 459 | ======================= 460 | 461 | This section defines the format of the source data. Note that the leftmost column in your source data is column 0. 462 | 463 | All of these are always required. 464 | 465 | --------------- 466 | timestamp index 467 | --------------- 468 | 469 | index of the timestamps in the original data. 470 | 471 | ---------------- 472 | wind speed index 473 | ---------------- 474 | 475 | index of wind speed 476 | 477 | -------------------- 478 | wind direction index 479 | -------------------- 480 | 481 | index of wind direction 482 | 483 | ----------------- 484 | temperature index 485 | ----------------- 486 | 487 | index of temperature measurements. Temperature needs to be in degrees Celsius. 488 | 489 | ----------- 490 | power index 491 | ----------- 492 | 493 | Index of output power measurement in source data. (Preferably in kilowatts, the units are assumed in some places when formatting output files.) 494 | 495 | Note: if source data uses relative values of output power the ice detection methods in the scripts do still work. The overall values for lost production might not make sense, but the timing of the icing events can still be calculated. 496 | 497 | ----------- 498 | rated power 499 | ----------- 500 | 501 | rated power of the turbine. 502 | 503 | ----------- 504 | state index 505 | ----------- 506 | 507 | indexes of state values or status codes used in data filtering. These can be found in multiple columns, just put everything here separated by commas i.e.:: 508 | 509 | state index = 8,9,10 510 | 511 | ------------ 512 | normal state 513 | ------------ 514 | 515 | The value of the state variable in so called normal state, used for filtering the data. This can be text or a number just use the same format as in the source data. Also you can specify multiple values here, just write them all on one line separated by commas. 516 | 517 | set these in same order as the state index above. If you want to include multiple valid values for one state variable add the appropriate index into state index once for each required value. 518 | 519 | Note: if the actual code contains a comma, the code will interpret that as two separate values and will crash. 520 | 521 | -------------- 522 | site elevation 523 | -------------- 524 | 525 | site elevation in meters above sea level, used for correcting the wind measurements. 526 | 527 | ------------ 528 | status index 529 | ------------ 530 | 531 | Index of the status signal. Used for collecting statistics of known stops 532 | 533 | ---------------------- 534 | status code stop value 535 | ---------------------- 536 | 537 | Value of the status code that indicates that the turbine has stopped. 538 | 539 | ============== 540 | Section: Icing 541 | ============== 542 | 543 | If the turbines on the site have ice detection or some kind of ice prevention system (anti- or de-icing) the code can take this into account and produce statistics of the Ice prevention system operation. 544 | 545 | This section is not mandatory, if there is no ice detector or no blade heating available. If ``Icing`` as a section is included, then all of these need to be defined as well. 546 | 547 | ------------- 548 | Ice detection 549 | ------------- 550 | 551 | Set this to ``True`` if there is an ice detection signal in the data. Leave the value to ``False`` if not. Used for collecting production statistics. This only cares about the presence of an explicit ice detection signal, sometimes a heated site might not have a visible ice detection signal in the data. 552 | 553 | ---------------- 554 | icing alarm code 555 | ---------------- 556 | 557 | Code in the data that corresponds to icing alarm. 558 | 559 | ----------------- 560 | icing alarm index 561 | ----------------- 562 | 563 | Zero-based index of the icing alarm code 564 | 565 | ------- 566 | heating 567 | ------- 568 | 569 | Set to ``True`` if site has blade heating. 570 | 571 | --------------- 572 | ips status code 573 | --------------- 574 | 575 | The code in the data that indicates that blade heating is on. 576 | 577 | ---------------- 578 | ips status index 579 | ---------------- 580 | 581 | Zero-based index of the ips status code 582 | 583 | --------------- 584 | ips status type 585 | --------------- 586 | 587 | Sets the type of the ips status code. Set to 1 if the ips status code value defined in ``ips status code`` indicates that ips is on and the blade heating is active. If this is set to 2 the code interprets all other values except the value in ``ips status code`` as blade heating being on. 588 | 589 | --------------------------- 590 | ips power consumption index 591 | --------------------------- 592 | 593 | If ips power measurement exists in the data, use this to give the index of the power consumption signal (zero-based). If there is no power consumption signal in the data, set this value to -1. 594 | 595 | 596 | ================ 597 | Section: Binning 598 | ================ 599 | 600 | Sets the binning options for the power curve calculations. 601 | 602 | This is not required. 603 | 604 | ------------------ 605 | minimum wind speed 606 | ------------------ 607 | 608 | minimum wind speed, all values below this will be sorted in the firs bin. Usually set to 0. Defaults to 0, if not set. 609 | 610 | ------------------ 611 | maximum wind speed 612 | ------------------ 613 | 614 | Maximum wind speed for the power curve, all values above this will end up in the last bin. Default value 20. 615 | 616 | ------------------- 617 | wind speed bin size 618 | ------------------- 619 | 620 | 621 | Wind speed bin size in meters per second. Default value 1. 622 | 623 | ----------------------- 624 | wind direction bin size 625 | ----------------------- 626 | 627 | Wind direction bin size in degrees. 628 | 629 | **NOTE:** If you do not want to use wind direction based binning set the bin size to 360 degrees. 630 | 631 | Default is set 360 i.e. no direction-based binning is used by default. 632 | 633 | ================== 634 | Section: Filtering 635 | ================== 636 | 637 | Data is filtered prior to analysis. The options for the filter are set in this section. 638 | 639 | ---------------- 640 | power drop limit 641 | ---------------- 642 | 643 | Lower limit for the power curve, defaults to `10` meaning using the P10 value to indicate the lower limit value used for ice detection. 644 | 645 | -------------------- 646 | overproduction limit 647 | -------------------- 648 | 649 | upper limit for normal operation. Used to mark overproduction in the data, defaults to `90` corresponding to top 90 percentile. 650 | 651 | ----------------- 652 | icing time filter 653 | ----------------- 654 | 655 | Number of continuous samples required to be under the lower limit in order to indicate an icing event has started. 656 | 657 | Note: this is number of samples, so for ten-minute data use 3 for 30 minutes and so on. Default value is 3. 658 | 659 | ---------------- 660 | stop filter type 661 | ---------------- 662 | 663 | Sets the source of what is counted as an icing induced turbine stop when calculating icing events. Stop filter here refers to an extra filtering step that can be used to remove turbine stops from the data if there is status code information that indicates that the turbine was stopped for reasons other than icing. 664 | Can have three different values: 665 | 666 | 0. Power level based filter (default). No extra filtering. 667 | 1. Status code stop. If the value of ``stop filter type`` is `1` filter out the bits where the status code in column set by ``status index`` is set to value defined by ``status code stop value`` 668 | 2. Status code normal operational state. If the value of ``stop filter type`` is `2`, keep only the parts of data where ``status index`` is set to value defined by ``status code stop value`` 669 | 670 | In case `2` ``status code stop value`` refers to turbine normal state. 671 | 672 | 673 | ---------------- 674 | stop time filter 675 | ---------------- 676 | 677 | Time filter used in stop detection. This is also the number of consecutive samples. Default value 6. 678 | 679 | 680 | ---------------- 681 | statefilter type 682 | ---------------- 683 | 684 | 685 | sets the filtering rule used to filter the data according to the state variable set earlier. State filter has four options 686 | 687 | 1. inclusive: Default value, keep only the part of the data where the state variable matches the defined normal state 688 | 2. exclusive: remove all data where state variable matches the defined normal state 689 | 3. greater than: keep only lines of data where state filter value greater than or equal to the value set 690 | 4. less than: keep only values where ste filter value is less than or equal to the value set 691 | 692 | The name ``normal state`` for the filtering variable can be misleading due to option 2 here. 693 | In the :ref:`input-data-example` you could filter based on column 6 using option 1 setting the normal value to ``OK``. 694 | 695 | 696 | ------------------ 697 | power level filter 698 | ------------------ 699 | 700 | Filter limit to remove stoppages from data. A power multiplier, defaults to 0.01. Power level filtering is used in order to remove times when turbine is stopped from the data. Useful if for example no turbine state information is known. This is applied to data 701 | 702 | --------------------- 703 | reference temperature 704 | --------------------- 705 | 706 | Initial reference data set is created by filtering out all measurements where temperature is below this limit. Defaults to 3 degrees Celsius. 707 | 708 | 709 | ------------------ 710 | temperature filter 711 | ------------------ 712 | 713 | Temperature limit for ice detection. If production is below the limit set in ``power drop limit`` **and** temperature is below the value set here, events are classified as icing. Default value is 1. 714 | 715 | ---------- 716 | icing time 717 | ---------- 718 | 719 | Minimum time needed to trigger an icing event. If production is below the designated level for at least the **number of samples** defined here and temperature is below the limit set with ``temperature filter``, an icing alarm is triggered. 720 | 721 | ---------------- 722 | stop time filter 723 | ---------------- 724 | 725 | When calculating stops from production, the production needs to be below the value defined in ``stop limit multiplier`` for at least the **number of samples** defined here in order to declare the samples as an icing induced stop. Default value is 3. 726 | 727 | 728 | --------------------- 729 | stop limit multiplier 730 | --------------------- 731 | 732 | Multiplier to define the lower limit for power. If output power is below this times nominal power the turbines is determined to have stopped. Defaults to 0.005 733 | 734 | 735 | ------------ 736 | min bin size 737 | ------------ 738 | 739 | Minimum sample count in a single bin when creating power curves. Defaults to 36. 740 | 741 | 742 | --------------- 743 | distance filter 744 | --------------- 745 | 746 | set this to ``True`` to add an additional filtering step to power curve calculation. This can improve results in most cases, on by default. Can be removed by setting ``distance filter = False`` 747 | 748 | ---------- 749 | start time 750 | ---------- 751 | 752 | If you want to calculate icing events and their losses to a period other than the whole data set, you can specify a different start time for your analysis. This uses same formatting that is specified in Section: Source file under :ref:`datetime-format`. 753 | 754 | If you want to use the data set from the beginning write ``NONE`` here in all caps. Set to ``NONE`` by default. 755 | 756 | --------- 757 | stop time 758 | --------- 759 | 760 | If you want to calculate icing events and their losses to a period other than the whole data set, you can specify a different stop time for your analysis. This uses same formatting that is specified in Section: Source file under :ref:`datetime-format`. 761 | 762 | If you want to use the data set till the end write ``NONE`` here in all caps. Set to ``NONE`` by default. 763 | 764 | ================ 765 | Mandatory values 766 | ================ 767 | 768 | The following values need to be set for every dataset. 769 | 770 | * Section: Source file: 771 | 772 | * id 773 | * filename 774 | * fault columns 775 | 776 | * Section: Data Structure: 777 | 778 | * timestamp index 779 | * wind speed index 780 | * wind direction index 781 | * temperature index 782 | * power index 783 | * rated power 784 | * state index 785 | * normal state 786 | * site elevation 787 | * status index 788 | * status code stop value 789 | 790 | 791 | 792 | ============== 793 | Default values 794 | ============== 795 | 796 | Set defaults are listed below: 797 | 798 | * Section 'Source file': 799 | 800 | * delimiter: ',' 801 | * quotechar: 'NONE' 802 | * datetime format: '%Y-%m-%d %H:%M:%S' 803 | * datetime extra char: '0' 804 | * replace fault codes': 'False' 805 | 806 | * Section 'Output': 807 | 808 | * result directory: '.' 809 | * summary: 'True', 810 | * plot: 'True', 811 | * alarm time series: 'True', 812 | * filtered raw data: 'True', 813 | * icing events: 'True' 814 | * power curve: 'True' 815 | 816 | * Section 'Binning': 817 | 818 | * minimum wind speed: '0', 819 | * maximum wind speed: '20', 820 | * wind speed bin size: '1', 821 | * wind direction bin size: '360' 822 | 823 | * Section: 'Filtering': 824 | 825 | * power drop limit: '10', 826 | * overproduction limit: '90', 827 | * power level filter: '0.01', 828 | * temperature filter: '1', 829 | * reference temperature: '3', 830 | * icing time: '3', 831 | * stop filter type: '0', 832 | * stop limit multiplier: '0.005', 833 | * stop time filter: '6', 834 | * statefilter type: '1', 835 | * min bin size: '36', 836 | * distance filter: 'True', 837 | * start time: 'None', 838 | * stop time: 'None' 839 | 840 | 841 | 842 | 843 | ****************** 844 | Wind park analysis 845 | ****************** 846 | 847 | The script by itself only operates on one time series (one turbine) at a time. If you are dealing with a data set that contains more than one turbine, using this scrip requires that you write a separate .ini file for each turbine. After this it is possible to write a small script or a batch file that runs the script for each turbine separately. One such example is included in the release .zip as ``multifile_t19_example.py`` 848 | 849 | This script also combines the summary files into one for easier comparison between the turbines. 850 | 851 | 852 | -------------------------------------------------------------------------------- /t19_ice_loss/data_file_handler.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import datetime 4 | import csv 5 | import numpy as np 6 | import json 7 | import configparser 8 | 9 | 10 | 11 | class CSVimporter: 12 | """ 13 | sets up an importer that reads in a set of data from a predefined .csv file 14 | 15 | """ 16 | def __init__(self,inputfilename = ''): 17 | """ 18 | Initialize the input reader 19 | assumes that data is in a text file where the leftmost field is a timestamp 20 | 21 | There are some class parameters that need to be set: 22 | 23 | delim : set the delimiter character between columns 24 | quote_char : charcter used to indicate a text field " by default 25 | if no quote character is used set this to None (note spelling) 26 | dt_format: formatting of the date, follows the convention of Python standard datetime formatting 27 | see https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior 28 | 29 | also initialises headders and data structures to empty ones 30 | 31 | :param inputfilename: relative path of the datafile 32 | :return: 33 | """ 34 | self.id = '' # data id used to id the data 35 | self.filename = inputfilename # path of the file 36 | self.delim = ',' 37 | self.quote_char = None # if no quotechar is used set this to None 38 | self.dt_format = '%Y-%m-%d %H:%M:%S' # follows the standard python datetime formatting 39 | self.dt_extra_char = 0 # extra characters i.e. timezone identifier etc. at the end of timestamp 40 | self.headers = [] 41 | self.full_data = [] 42 | self.replace_faults = False # Data processing chokes on non-numeric values so textual fault codes need to be replaced 43 | self.fault_columns = [] 44 | self.fault_dict = {} 45 | self.result_dir = '.' 46 | self.timestamp_index = 0 47 | self.summaryfile_write = True 48 | self.pc_plot_picture = True 49 | self.alarm_time_series_file_write = False 50 | self.filtered_raw_data_write = False 51 | self.icing_events_write = False 52 | self.power_curve_write = True 53 | 54 | def read_file_options_from_file(self,config_filename): 55 | """ 56 | set file options from a config file see the documentation for full listing of options 57 | 58 | :param config_filename: name and full path of the config file 59 | 60 | 61 | """ 62 | config = configparser.ConfigParser() 63 | config.read(config_filename) 64 | try: 65 | self.id = config.get('Source file','id') 66 | self.filename = config.get('Source file','filename') 67 | delimiter = config.get('Source file','delimiter', fallback=',') 68 | if delimiter == 'TAB': 69 | self.delim = '\t' 70 | else: 71 | self.delim = delimiter 72 | quot_char = config.get('Source file','quotechar', fallback=None) 73 | if (quot_char is None) or (quot_char.upper() == 'NONE'): 74 | self.quote_char = None 75 | else: 76 | self.quote_char = quot_char 77 | self.dt_format = config.get('Source file','datetime format',raw = True, fallback='%Y-%m-%d %H:%M:%S') 78 | self.dt_extra_char = int(config.get('Source file','datetime extra char', fallback=0)) 79 | fault_column_string = config.get('Source file','fault columns') 80 | self.fault_columns = [int(column_index) for column_index in fault_column_string.split(',')] 81 | self.replace_faults = config.getboolean('Source file','replace fault codes',fallback=False) 82 | skip_column_string = config.get('Source file','Skip columns') 83 | if skip_column_string == "NONE": 84 | self.skip_columns = [] 85 | else: 86 | self.skip_columns = [int(column_index) for column_index in skip_column_string.split(',')] 87 | self.result_dir = config.get('Output','result directory',fallback='.') 88 | self.summaryfile_write = config.getboolean('Output', 'summary', fallback=True) 89 | self.pc_plot_picture = config.getboolean('Output', 'plot', fallback=True) 90 | self.alarm_time_series_file_write = config.getboolean('Output', 'alarm time series', fallback=False) 91 | self.filtered_raw_data_write = config.getboolean('Output', 'filtered raw data', fallback=False) 92 | self.icing_events_write = config.getboolean('Output', 'icing events', fallback=False) 93 | self.power_curve_write = config.getboolean('Output', 'power curve', fallback=True) 94 | self.timestamp_index = int(config.get('Data Structure','timestamp index')) 95 | except configparser.NoOptionError as missing_value: 96 | print("missing config option: {0} in {1}".format(missing_value, config_filename)) 97 | except ValueError as wrong_value: 98 | print("Wrong type of value in {0}: {1}".format(config_filename, wrong_value)) 99 | 100 | 101 | 102 | def create_new_faultcodes(self, column_num, write_to_file = False, outfilename = ''): 103 | """ 104 | Generate replacement faultcodes from the data in case faultcodes are in some kind of alphanumeric format i.e. not numbers 105 | 106 | column num is the column that contains all the fault codes. 107 | 108 | :param column_num: column index that contains the fault codes 109 | :param write_to_file: if True, resutls written to disk into file specified by _outfilename_ 110 | :param outfilename: name of the output file 111 | :return: fault_dict a python dictionary containig all discovered fault codes (dictionary keys) and numbers to replace them with (dictionary values) 112 | 113 | """ 114 | inputfile = open(self.filename,'r') 115 | file_reader = csv.reader(inputfile,delimiter = self.delim, quotechar = self.quote_char) 116 | textdata = [] 117 | headers = next(file_reader) 118 | columns = len(headers) 119 | fault_codes = [] 120 | while True: 121 | try: 122 | # data_row = (list(map(str.strip,next(file_reader)))) 123 | data_row = next(file_reader) 124 | if data_row[column_num] not in fault_codes: 125 | fault_codes.append(data_row[column_num].strip()) 126 | except StopIteration: 127 | break 128 | inputfile.close() 129 | # rows=len(textdata) 130 | # ndata = np.array(textdata) 131 | # print(ndata[1]) 132 | # fault_codes = np.unique(ndata[:,column_num]) 133 | fault_dict = {} 134 | for index,code in enumerate(fault_codes): 135 | fault_dict[code] = index 136 | if write_to_file: 137 | self.write_fault_dict(outfilename, fault_dict) 138 | return fault_dict 139 | 140 | def read_fault_codes(self,infilename): 141 | """ 142 | read fault codes from a previously generated .json file 143 | 144 | :param infilename: name of the input file 145 | :return: fault_dict a python dictionary containig all discovered fault codes (dictionary keys) and numbers to replace them with (dictionary values) 146 | 147 | """ 148 | infile = open(infilename,'r') 149 | fault_dict = json.load(infile) 150 | infile.close() 151 | return fault_dict 152 | 153 | def write_fault_dict(self,outfilename,fault_dict): 154 | """ 155 | writes a fault dictionary on disk as a .json file 156 | 157 | :param outfilename: name of the output filename 158 | :param fault_dict: fault code dictionary 159 | 160 | """ 161 | outfile = open(outfilename,'w') 162 | json.dump(fault_dict, outfile, indent=4, sort_keys= True) 163 | outfile.close() 164 | 165 | 166 | def update_fault_codes(self, column_num, infilename, write_to_file=False, outfilename=''): 167 | """ 168 | updates an already saved list of fault codes from the inputfile 169 | 170 | :param column_num: column index of the fault variable 171 | :param infilename: name of the input file 172 | :param write_to file: toggles writing the updated fault dictionary to a file 173 | :param outfilename: nae of the output file 174 | :return: the updated fault dictionary 175 | 176 | """ 177 | fault_dict = self.read_fault_codes(infilename) 178 | new_codes = self.create_new_faultcodes(column_num, write_to_file,outfilename) 179 | next_code = max(fault_dict.values()) + 1 180 | for item in new_codes: 181 | if item not in fault_dict.keys(): 182 | fault_dict[item] = next_code 183 | next_code += 1 184 | if write_to_file: 185 | self.write_fault_dict(outfilename, fault_dict) 186 | return fault_dict 187 | 188 | 189 | def is_float(self,char_string): 190 | """ 191 | checks whether or not a character string can be converted into a float 192 | 193 | :param char_string: 194 | :return: status of conversion 195 | 196 | """ 197 | try: 198 | float(char_string) 199 | return True 200 | except ValueError: 201 | return False 202 | 203 | 204 | def create_faultfile(self): 205 | """ 206 | Creates a filename for the fault file based on the name of the input file 207 | 208 | :return: filename of the fault dictionary 209 | 210 | """ 211 | faultfilename = self.result_dir + self.id + '_faults.json' 212 | return faultfilename 213 | 214 | def process_fault_codes(self): 215 | """ 216 | create a data structure that can be used to replace textual fault codes in the data that is read in for processing 217 | TODO: Currently, only acceptable values are those that are found in the file itself. This should be changed 218 | to allow seeding of the values that are set in the .ini file. You can't set a value for this 219 | that is not found in a file. i.e. if you have a fault indicator value "FAULT" and the data does not have any 220 | lines with that value, the code will crash. 221 | This can be changed ether here or at the point where fault_dict is accessed, you check if value exists, 222 | if not then you add it. Requires ability to manually add values to fault_dict. 223 | """ 224 | fault_code_filename = self.create_faultfile() 225 | if len(self.fault_columns) == 1: 226 | self.fault_dict = self.create_new_faultcodes(self.fault_columns[0], True, fault_code_filename) 227 | else: 228 | for i,col in enumerate(self.fault_columns): 229 | if i == 0: 230 | self.fault_dict = self.create_new_faultcodes(col, True, fault_code_filename) 231 | else: 232 | self.fault_dict = self.update_fault_codes(col, fault_code_filename, True, fault_code_filename) 233 | 234 | 235 | def read_data(self): 236 | """ 237 | read pre-specified .csv formatted datafile, return a numpy nparray of data in format: 238 | 239 | Specifications of the original file are defined as class variables. 240 | 241 | sets values of self.data and self.headers according to the contents of the file 242 | 243 | 244 | [timestamp, value, ...] 245 | """ 246 | datafile = open(self.filename,'r') 247 | inputdata = csv.reader(datafile,delimiter = self.delim,quotechar=self.quote_char) 248 | # TODO: 249 | # currently assumes that the first row and only the first row of the file has 250 | # header information related to the file contents 251 | # needs to be fixed to be adjustable, files can contain 0..n rows of headers 252 | headers = next(inputdata) 253 | full_data = [] 254 | line_number = 1 255 | dataline = [] 256 | if self.replace_faults: 257 | self.process_fault_codes() 258 | # print(self.fault_dict) 259 | while True: 260 | try: 261 | dataline = next(inputdata) 262 | outputline = [] 263 | for i in range(0,len(dataline)): 264 | if i in self.skip_columns: 265 | outputline.append(np.nan) 266 | elif i == self.timestamp_index: 267 | ts_string = dataline[self.timestamp_index] 268 | if self.dt_extra_char == 0: 269 | ts = datetime.datetime.strptime(ts_string,self.dt_format) 270 | else: 271 | ts = datetime.datetime.strptime(ts_string[:-self.dt_extra_char],self.dt_format) 272 | outputline.append(ts) 273 | elif self.replace_faults and (i in self.fault_columns): 274 | outputline.append(self.fault_dict[dataline[i].strip()]) 275 | elif self.is_float(dataline[i]): 276 | outputline.append(float(dataline[i])) 277 | else: 278 | if self.is_float(dataline[i]): 279 | outputline.append(float(dataline[i])) 280 | else: 281 | if 'FALSE' in dataline[i].upper(): 282 | outputline.append(False) 283 | elif 'TRUE' in dataline[i].upper(): 284 | outputline.append(True) 285 | else: 286 | outputline.append(np.nan) 287 | full_data.append(outputline) 288 | line_number += 1 289 | except StopIteration: 290 | print("{0} : File {1} read".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),self.filename)) 291 | break 292 | except csv.Error as e: 293 | print("{0} : Error {1} while reading file {2}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),e,self.filename)) 294 | except ValueError as e: 295 | print("{0} : Error {1} while reading file {2}".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),e,self.filename)) 296 | print("Error on line: {0}".format(line_number)) 297 | print(dataline) 298 | full_data_a = np.array(full_data) 299 | # remove duplicate timestamps 300 | # check for unique lines in timestamp column 301 | full_data_as = full_data_a[full_data_a[:,self.timestamp_index].argsort(),:] 302 | 303 | uts, inds,inve = np.unique(full_data_as[:, self.timestamp_index], return_index=True, return_inverse=True) 304 | # use only indexes of unique timestamps 305 | full_data_au = full_data_as[inds, :] 306 | # sort according to timestamp 307 | 308 | datafile.close() 309 | self.headers = headers 310 | self.full_data = full_data_au 311 | 312 | class Result_file_writer(): 313 | """ 314 | sets up a writer to deal with results of the counter 315 | """ 316 | def __init__(self): 317 | self.result_dir = './' 318 | self.summaryfile_write = True 319 | self.pc_plot_picture = True 320 | self.alarm_time_series_file_write = False 321 | self.filtered_raw_data_write = False 322 | self.icing_events_write = False 323 | self.power_curve_write = True 324 | 325 | 326 | def set_output_file_options(self, config_filename): 327 | """ 328 | read the parameters set in .ini file for the Output section. These are used to select what outputs will be written and what not 329 | 330 | :param config_filename: Name of the config file used 331 | """ 332 | config = configparser.ConfigParser() 333 | config.read(config_filename) 334 | try: 335 | self.result_dir = config.get('Output', 'result directory', fallback='./') 336 | self.summaryfile_write = config.getboolean('Output', 'summary', fallback=True) 337 | self.pc_plot_picture = config.getboolean('Output', 'plot', fallback=True) 338 | self.alarm_time_series_file_write = config.getboolean('Output', 'alarm time series', fallback=False) 339 | self.filtered_raw_data_write = config.getboolean('Output', 'filtered raw data', fallback=False) 340 | self.icing_events_write = config.getboolean('Output', 'icing events', fallback=False) 341 | self.power_curve_write = config.getboolean('Output', 'power curve', fallback=True) 342 | self.power_curve_plot_max = int(config.get('Data Structure', 'maximum wind speed', fallback='20')) 343 | except configparser.NoOptionError as missing_value: 344 | print("missing config option: {0} in {1}".format(missing_value, config_filename)) 345 | except ValueError as wrong_value: 346 | print("Wrong type of value in {0}: {1}".format(config_filename, wrong_value)) 347 | 348 | def write_alarm_file(self, result_filepath, array): 349 | """ 350 | writes the alarm timeseries in a file in working directory 351 | 352 | :param result_filepath: full filename 353 | :param array: data array 354 | :return: status of write operation, possible error message 355 | 356 | """ 357 | fields = ['timestamp', 'alarm status', 'wind speed [m/s]', 'reference power [kW]', 'temperature [C]', 'power [kW]', 'P10 limit [kW]'] 358 | try: 359 | with open(result_filepath, 'w', newline='') as result_file: 360 | writer = csv.writer(result_file, delimiter=';') 361 | writer.writerow(fields) 362 | writer.writerows(array) 363 | return True ,'' 364 | except IOError as e: 365 | return False, e 366 | 367 | 368 | def write_time_series_file(self, result_filepath, array, headers,aepc,pc): 369 | """ 370 | writes a data timeseries in a file in working directory 371 | 372 | :param result_filepath: 373 | :param array: 374 | :param headers: 375 | :return: status of the writing, possible error 376 | 377 | """ 378 | reference_power = aepc.theoretical_output_power(array,pc) 379 | out_array = [] 380 | for i,line in enumerate(array): 381 | out_array.append(np.hstack((line, reference_power[i,1], reference_power[i,3], reference_power[i,4]))) 382 | headers.append('Reference Power') 383 | headers.append('P10 Limit') 384 | headers.append('P90 Limit') 385 | out_array = np.array(out_array) 386 | try: 387 | with open(result_filepath, 'w', newline='') as result_file: 388 | writer = csv.writer(result_file, delimiter=';') 389 | writer.writerow(headers) 390 | writer.writerows(out_array) 391 | return True,'' 392 | except IOError as e: 393 | return False, e 394 | 395 | 396 | def write_alarm_timings(self, result_filepath, array): 397 | """ 398 | writes a data timeseries in a file in working directory 399 | 400 | :param result_filepath: path of result file 401 | :param array: data array 402 | :return: status of writing, error 403 | 404 | """ 405 | headers = ['start', 'stop', 'loss', 'duration', 'mean power drop', 'mean_power', 'mean_reference_power', 406 | 'mean wind speed', 'mean temperature'] 407 | # headers = ['Event start', 'Event stop', 'loss [kWh]', 'duration [h]'] 408 | try: 409 | with open(result_filepath, 'w', newline='') as result_file: 410 | writer = csv.writer(result_file, delimiter=';') 411 | writer.writerow(headers) 412 | writer.writerows(array) 413 | return True, '' 414 | except IOError as e: 415 | return False, e 416 | 417 | 418 | def summary_statistics(self, aepc, data, reference_data, pc, alarm_timings, stop_timings, over_timings, status_timings, ice_timings, ips_timings, data_sizes): 419 | """ 420 | Calculate summary statistics for the dataset. contains: 421 | availability 422 | data loss due to filtering 423 | size of the reference dataset 424 | hour counts for different ice classes 425 | production losses due to different causes 426 | Theoretical maximum production 427 | observed production 428 | losses due to all reasons 429 | Written to a file 430 | 431 | :param aepc: the active aeoc object 432 | :param data: data used to calculate statistics 433 | :param reference_data: the reference dataset used to calculate power curve 434 | :param pc: power curve structure 435 | :param alarm_timings: reduced power incidents 436 | :param stop_timings: icing induced stops 437 | :param over_timings: overproduction incidents 438 | :param data_sizes: sizes after each filtering step 439 | :return: status of the write operation, full filename ,possible error 440 | 441 | """ 442 | if aepc.starttimestamp == datetime.datetime.min: 443 | start_time = data[0,aepc.ts_index] 444 | else: 445 | start_time = aepc.starttimestamp 446 | if aepc.stoptimestamp == datetime.datetime.max: 447 | stop_time = data[-1,aepc.ts_index] 448 | else: 449 | stop_time = aepc.stoptimestamp 450 | data_period = (stop_time-start_time).total_seconds()/60.0/60.0 451 | reference_start = reference_data[0,aepc.ts_index] 452 | reference_stop = reference_data[-1,aepc.ts_index] 453 | reference_data_period = (reference_stop-reference_start).total_seconds()/60.0/60.0 454 | step_size = data[1, aepc.ts_index] - data[0, aepc.ts_index] 455 | #check for empty array (no stops) 456 | if np.shape(stop_timings) == (0,): 457 | stop_losses = 0.0 458 | stop_duration = 0.0 459 | else: 460 | stop_losses = np.nansum(stop_timings[:, 2]) 461 | stop_duration = np.nansum(stop_timings[:, 3]) 462 | # check for empty 463 | if np.shape(alarm_timings) == (0,): 464 | icing_loss_production = 0.0 465 | icing_duration = 0.0 466 | else: 467 | icing_loss_production = np.nansum(alarm_timings[:, 2]) 468 | icing_duration = np.nansum(alarm_timings[:, 3]) 469 | # check for empty 470 | if np.shape(over_timings) == (0,): 471 | over_prod_duration = 0.0 472 | else: 473 | over_prod_duration = np.nansum(over_timings[:, 3]) 474 | # check for empty 475 | if (np.shape(status_timings) == (0,)) or (status_timings is None): 476 | status_stop_duration = 0.0 477 | status_stop_loss = 0.0 478 | else: 479 | status_stop_loss = np.nansum(status_timings[:, 2]) 480 | status_stop_duration = np.nansum(status_timings[:, 3]) 481 | if (np.shape(ice_timings) == (0,)) or (ice_timings is None): 482 | ice_detection_duration = 0.0 483 | ice_detection_loss = 0.0 484 | else: 485 | ice_detection_loss = np.nansum(ice_timings[:, 2]) 486 | ice_detection_duration = np.nansum(ice_timings[:, 3]) 487 | if (np.shape(ips_timings) == (0,)) or (ips_timings is None): 488 | ips_on_duration = 0.0 489 | ips_on_production_loss = 0.0 490 | ips_self_consumption = 0.0 491 | else: 492 | ips_on_production_loss = np.nansum(ips_timings[:,2]) 493 | ips_on_duration = np.nansum(ips_timings[:,3]) 494 | ips_self_consumption = np.nansum(ips_timings[:,4]) 495 | 496 | uncertainty = aepc.power_curve_uncertainty_average(pc) 497 | 498 | tmax_power = aepc.theoretical_output_power(data, pc) 499 | if np.shape(tmax_power) == (0,): 500 | theoretical_production_sum = 0.0 501 | actual_production_sum = 0.0 502 | min_production_sum = 0.0 503 | max_production_sum = 0.0 504 | total_losses = 0.0 505 | energy_based_avail = 0.0 506 | icing_loss_perc = 0.0 507 | stop_loss_perc = 0.0 508 | icing_duration_perc = 0.0 509 | stop_duration_perc = 0.0 510 | over_prod_duration_perc = 0.0 511 | technical_availability = 0.0 512 | status_stop_loss_perc = 0.0 513 | ice_detection_duration_perc = 0.0 514 | ice_detection_loss_perc = 0.0 515 | ips_on_duration_perc = 0.0 516 | ips_on_loss_perc = 0.0 517 | else: 518 | theoretical_production = aepc.calculate_production(tmax_power, 1) 519 | actual_production = aepc.calculate_production(tmax_power, 2) 520 | production_p10 = aepc.calculate_production(tmax_power, 3) 521 | production_p90 = aepc.calculate_production(tmax_power, 4) 522 | min_production = aepc.calculate_production(tmax_power, 5) 523 | max_production = aepc.calculate_production(tmax_power, 6) 524 | theoretical_production_sum = np.nansum(theoretical_production[:, 1]) 525 | actual_production_sum = np.nansum(actual_production[:, 1]) 526 | min_production_sum = np.nansum(min_production[:, 1]) 527 | max_production_sum = np.nansum(max_production[:, 1]) 528 | production_sum_p10 = np.nansum(production_p10[:, 1]) 529 | production_sum_p90 = np.nansum(production_p90[:, 1]) 530 | production_upper_limit = max_production_sum / theoretical_production_sum * 100.0 531 | production_lower_limit = min_production_sum / theoretical_production_sum * 100.0 532 | production_p10_limit = production_sum_p10 / theoretical_production_sum * 100.0 533 | production_p90_limit = production_sum_p90 / theoretical_production_sum * 100.0 534 | total_losses = theoretical_production_sum - actual_production_sum 535 | energy_based_avail = 100.0 - ((total_losses/theoretical_production_sum) * 100.0) 536 | icing_loss_perc = (icing_loss_production/actual_production_sum) * 100.0 537 | stop_loss_perc = (stop_losses/actual_production_sum) * 100.0 538 | status_stop_loss_perc = (status_stop_loss/actual_production_sum) * 100.0 539 | icing_duration_perc = (icing_duration / data_period) * 100.0 540 | stop_duration_perc = (stop_duration / data_period) * 100.0 541 | over_prod_duration_perc = (over_prod_duration / data_period) * 100.0 542 | technical_availability = ((data_period - status_stop_duration) / data_period) * 100.0 543 | ice_detection_duration_perc = (ice_detection_duration / data_period) * 100.0 544 | ice_detection_loss_perc = (ice_detection_loss / actual_production_sum) * 100.0 545 | ips_on_duration_perc = (ips_on_duration / data_period) * 100.0 546 | ips_on_loss_perc = (ips_on_production_loss / actual_production_sum) * 100.0 547 | ips_self_consumption_perc = (ips_self_consumption / actual_production_sum) * 100.0 548 | # availability = aepc.count_availability(data) * 100.0 549 | availability = data_sizes[0] / ((stop_time - start_time) / step_size) * 100.0 550 | 551 | filtered_data_size = (data_sizes[1]/data_sizes[0]) * 100.0 552 | reference_data_size = (data_sizes[2]/data_sizes[0]) * 100.0 553 | 554 | filename_trunk = '_summary.txt' 555 | full_filename = aepc.result_dir + aepc.id + filename_trunk 556 | try: 557 | with open(full_filename,'w') as f: 558 | # f.write("Statistics from the dataset: {} \n".format(aepc.id)) 559 | # f.write("\n") 560 | # f.write("[Generic statistics] \n") 561 | # f.write("Data start: {0}, stop {1}; total: {2:.1f} hours \n" 562 | # .format(start_time.strftime("%Y-%m-%d %H:%M:%S"),stop_time.strftime("%Y-%m-%d %H:%M:%S"),data_period)) 563 | # f.write("Data availability: {:.1f} % \n".format(availability)) 564 | # f.write("Sample count in original data: {0}, after filtering: {1}, loss due to filtering: {2:.1f} % \n" 565 | # .format(data_sizes[0],data_sizes[1], data_loss)) 566 | # f.write("Sample count in reference data {0}, size of raw data {1:.1f} \n" 567 | # .format(data_sizes[2],reference_loss)) 568 | # f.write("\n") 569 | # f.write("[Production] \n") 570 | # f.write("Theoretical maximum production: {0:.1f}, observed power production: {1:.1f} \n" 571 | # .format(theoretical_production_sum, actual_production_sum)) 572 | # f.write("Total losses {0:.1f}, {1:.1f} % \n".format(total_losses, total_losses_perc)) 573 | # f.write("\n") 574 | # f.write("[Icing] \n") 575 | # f.write("Icing during production: {0:.1f} hours, {1:.1f} % of total data \n" 576 | # .format(icing_duration,icing_duration_perc)) 577 | # f.write("Icing induced stops: {0:.1f} hours, {1:.1f} % of total data \n" 578 | # .format(stop_duration,stop_duration_perc)) 579 | # f.write("Overproduction: {0:.1f} hours, {1:.1f} % of total data \n" 580 | # .format(over_prod_duration,over_prod_duration_perc)) 581 | # f.write("Production losses due to icing: {0:.1f}, {1:.1f} % \n" 582 | # .format(icing_loss_production, icing_loss_perc)) 583 | # f.write("Production losses during icing induced stops: {0:.1f}, {1:.1f} % \n" 584 | # .format(stop_losses, stop_loss_perc)) 585 | f.write("{heading: <{fill1}}\t {value: >{fill2}} \t{unit}\n".format(heading='Field',fill1=50,value='Value', fill2=20, unit='unit')) 586 | f.write("{heading: <{fill1}}\t {value: >{fill2}} \t{unit}\n".format(heading='Dataset name',fill1=50,value=aepc.id, fill2=20, unit=' ')) 587 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Production losses due to icing',fill1=50, value=icing_loss_production, fill2=20, unit='kWh')) 588 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Relative production losses due to icing',fill1=50, value=icing_loss_perc, fill2=20, unit='%')) 589 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Losses due to icing related stops',fill1=50, value=stop_losses, fill2=20, unit='kWh')) 590 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Relative losses due to icing related stops',fill1=50, value=stop_loss_perc, fill2=20, unit='%')) 591 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Icing during production',fill1=50, value=icing_duration, fill2=20, unit='h')) 592 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Icing during production (% of total data)',fill1=50, value=icing_duration_perc, fill2=20, unit='%')) 593 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Turbine stopped during production',fill1=50, value=stop_duration, fill2=20, unit='h')) 594 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Turbine stopped production (% of total data)',fill1=50, value=stop_duration_perc, fill2=20, unit='%')) 595 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Over production hours',fill1=50, value=over_prod_duration, fill2=20, unit='h')) 596 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Over production hours (% of total)',fill1=50, value=over_prod_duration_perc, fill2=20, unit='%')) 597 | if aepc.heated_site: 598 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='IPS on hours', fill1=50, value=ips_on_duration,fill2=20, unit='h')) 599 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='IPS on hours (% of total)', fill1=50, value=ips_on_duration_perc, fill2=20,unit='%')) 600 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Losses during IPS operation', fill1=50, value=ips_on_production_loss,fill2=20, unit='kWh')) 601 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Relative losses during IPS operation', fill1=50, value=ips_on_loss_perc, fill2=20,unit='%')) 602 | if aepc.ice_detection: 603 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Ice detector icing hours',fill1=50, value=ice_detection_duration, fill2=20, unit='h')) 604 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Ice detector icing hours (% of total data)',fill1=50, value=ice_detection_duration_perc, fill2=20, unit='%')) 605 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Losses during ice detector alarms',fill1=50, value=ice_detection_loss, fill2=20, unit='h')) 606 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Relative losses during ice detector alarm (% of total data)',fill1=50, value=ice_detection_loss_perc, fill2=20, unit='h')) 607 | if aepc.heating_power_index >= 0: 608 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='IPS self consumption',fill1=50, value=ips_self_consumption, fill2=20, unit='kWh')) 609 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='IPS self consumption (% of total)',fill1=50, value=ips_self_consumption_perc, fill2=20, unit='%')) 610 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='SCADA forced stops',fill1=50, value=status_stop_duration, fill2=20, unit='h')) 611 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Time Based Availability (TBA)',fill1=50, value=technical_availability, fill2=20, unit='%')) 612 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Loss during SCADA stops',fill1=50, value=status_stop_loss, fill2=20, unit='kWh')) 613 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Relative losses during SCADA stops (% of total)',fill1=50, value=status_stop_loss_perc, fill2=20, unit='%')) 614 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Power curve uncertainty',fill1=50, value=uncertainty, fill2=20, unit='%')) 615 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Production upper limit (std.dev)',fill1=50, value=production_upper_limit, fill2=20, unit='%')) 616 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Production lower limit (std.dev)',fill1=50, value=production_lower_limit, fill2=20, unit='%')) 617 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Production P90', fill1=50, value=production_p90_limit, fill2=20,unit='%')) 618 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Production P10', fill1=50, value=production_p10_limit, fill2=20,unit='%')) 619 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Theoretical mean production', fill1=50, value=theoretical_production_sum,fill2=20, unit='kWh')) 620 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Observed power production',fill1=50, value=actual_production_sum, fill2=20, unit='kWh')) 621 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Total Losses',fill1=50, value=total_losses, fill2=20, unit='kWh')) 622 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Energy Based Availability (EBA)',fill1=50, value=energy_based_avail, fill2=20, unit='%')) 623 | f.write("{heading: <{fill1}}\t {value:>{fill2}} \t{unit}\n".format(heading='Data start time',fill1=50, value=start_time.strftime("%Y-%m-%d %H:%M:%S"), fill2=20, unit=' ')) 624 | f.write("{heading: <{fill1}}\t {value:>{fill2}} \t{unit}\n".format(heading='Data stop time',fill1=50, value=stop_time.strftime("%Y-%m-%d %H:%M:%S"), fill2=20, unit=' ')) 625 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Total amount of data',fill1=50, value=data_period, fill2=20, unit='h')) 626 | f.write("{heading: <{fill1}}\t {value:>{fill2}} \t{unit}\n".format(heading='Reference data start time',fill1=50, value=reference_start.strftime("%Y-%m-%d %H:%M:%S"), fill2=20, unit=' ')) 627 | f.write("{heading: <{fill1}}\t {value:>{fill2}} \t{unit}\n".format(heading='Reference data stop time',fill1=50, value=reference_stop.strftime("%Y-%m-%d %H:%M:%S"), fill2=20, unit=' ')) 628 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Total amount of data in reference dataset',fill1=50, value=reference_data_period, fill2=20, unit='h')) 629 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Data availability',fill1=50, value=availability, fill2=20, unit='%')) 630 | f.write("{heading: <{fill1}}\t {value:>{fill2}d} \t{unit}\n".format(heading='Sample count in original data',fill1=50, value=data_sizes[0], fill2=20, unit=' ')) 631 | f.write("{heading: <{fill1}}\t {value:>{fill2}d} \t{unit}\n".format(heading='Sample count in after filtering',fill1=50, value=data_sizes[1], fill2=20, unit=' ')) 632 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Data size after filtering',fill1=50, value=filtered_data_size, fill2=20, unit='%')) 633 | f.write("{heading: <{fill1}}\t {value:>{fill2}d} \t{unit}\n".format(heading='Sample count in reference data',fill1=50, value=data_sizes[2], fill2=20, unit=' ')) 634 | f.write("{heading: <{fill1}}\t {value:>{fill2}.1f} \t{unit}\n".format(heading='Reference dataset as % of original data',fill1=50, value=reference_data_size, fill2=20, unit='%')) 635 | f.write(" \t \t \n") 636 | f.write(" \t \t \n") 637 | 638 | 639 | 640 | return True, full_filename , '' 641 | except IOError as e: 642 | return False, full_filename, e 643 | 644 | 645 | 646 | def write_power_curve(self, aepc, pc, pc_id=''): 647 | """ 648 | Write power curve, P10 and P90 into a file 649 | 650 | :param aepc: AEPCounter used to calculate the power curve 651 | :param pc: the actual power curve 652 | :return: status of the write operation, filename of the power curve file, possible error 653 | 654 | """ 655 | powercurve = aepc.prettyprint_power_curves(pc, index=2) 656 | p10 = aepc.prettyprint_power_curves(pc, index=3) 657 | p90 = aepc.prettyprint_power_curves(pc, index=4) 658 | std_dev = aepc.prettyprint_power_curves(pc, index=5) 659 | uncertainty = aepc.prettyprint_power_curves(pc, index=6) 660 | bin_size = aepc.prettyprint_power_curves(pc, index=7) 661 | lower_lim = aepc.prettyprint_power_curves(pc, index=8) 662 | upper_lim = aepc.prettyprint_power_curves(pc, index=9) 663 | filename_trunk = '_powercurve.txt' 664 | filename = aepc.result_dir + aepc.id + pc_id + filename_trunk 665 | try: 666 | with open(filename,'w') as f: 667 | f.write('{name} Power Curve\n'.format(name=aepc.id)) 668 | f.writelines(powercurve) 669 | f.write("\n") 670 | f.write('{name} P10 \n'.format(name=aepc.id)) 671 | f.writelines(p10) 672 | f.write("\n") 673 | f.write('{name} P90 \n'.format(name=aepc.id)) 674 | f.writelines(p90) 675 | f.write("\n") 676 | f.write('{name} Std.dev. \n'.format(name=aepc.id)) 677 | f.writelines(std_dev) 678 | f.write("\n") 679 | f.write('{name} Uncertainty [%] \n'.format(name=aepc.id)) 680 | f.writelines(uncertainty) 681 | f.write("\n") 682 | f.write('{name} Lower limit \n'.format(name=aepc.id)) 683 | f.writelines(lower_lim) 684 | f.write("\n") 685 | f.write('{name} Upper limit \n'.format(name=aepc.id)) 686 | f.writelines(upper_lim) 687 | f.write("\n") 688 | f.write('{name} Bin Size [n] \n'.format(name=aepc.id)) 689 | f.writelines(bin_size) 690 | f.write("\n") 691 | return True, filename, '' 692 | except IOError as e: 693 | return False, filename, e 694 | 695 | def write_monthly_stats(self, data, pc, aepc, ice_events, ice_stops, status_stops, ips_on_flags, ice_detected): 696 | """ 697 | write production loss statistics to file 698 | 699 | :param data: input data 700 | :param pc: calculated power curve 701 | :param aepc: aep counter used to calculate the stats 702 | :return: status of the write operation, filename, error 703 | """ 704 | production_statistics = aepc.calculate_production_stats(data, pc,ice_events, ice_stops, status_stops, ips_on_flags, ice_detected) 705 | filename_trunk = '_production_stats.txt' 706 | filename = aepc.result_dir + aepc.id + filename_trunk 707 | headers = ['month', 'Theoretical production', 'Actual production', 'Total losses', 'Total losses (%)', 708 | 'Production losses due to icing', 'Relative icing production loss', 709 | 'Losses due to icing induced stops', 'Relative losses due to iced stops', 710 | 'Losses during SCADA stops', 'Relative losses during SCADA stops', 711 | 'Losses during IPS operation', 'Relative losses during IPS operation', 712 | 'Losses during ice detection', 'Relative losses during ice detection', 713 | 'Total icing losses', 'Relative icing losses', 'IPS consumption'] 714 | try: 715 | with open(filename,'w') as f: 716 | for item in headers: 717 | f.write(item) 718 | f.write('\t') 719 | f.write('\n') 720 | for line in production_statistics: 721 | for item in line: 722 | if type(item) == datetime.datetime: 723 | f.write(item.strftime('%Y-%m')) 724 | f.write('\t') 725 | else: 726 | f.write(str(item)) 727 | f.write('\t') 728 | f.write('\n') 729 | return True, filename, '' 730 | except IOError as e: 731 | return False, filename, e 732 | 733 | 734 | 735 | def insert_fault_codes(self, data,aepc,reader): 736 | """ 737 | re-insert the textual fault codes into the data time series table 738 | 739 | :param data: input data 740 | :param aepc: aep counter object used 741 | :param reader: active CSVReader object 742 | :return: the filtered data as numpy.ndarray 743 | 744 | """ 745 | for k,line in enumerate(data): 746 | for i,item in enumerate(line): 747 | if i in reader.fault_columns: 748 | for code,val in reader.fault_dict.items(): 749 | if val == item: 750 | data[k,i] = code 751 | return data 752 | 753 | def generate_standard_plots(self, data, pc, aepc, red_power, overprod, stops, data_sizes, alarm_timings, over_timings, stop_timings, ips_on_flags, write=False): 754 | """ 755 | create two predefined plots from the time series data 756 | 757 | :param data: input data 758 | :param pc: power curve structure 759 | :param aepc: active AEP Counter object 760 | :param red_power: timeseries of reduced power 761 | :param overprod: timeseries of overproduction 762 | :param stops: timeseries of stops 763 | :param data_sizes: lengths of differently filtered datasets 764 | :param alarm_timings: statistics of reduced power incidents 765 | :param over_timings: statistics for over production 766 | :param stop timigns: statistics of icing induced stop events 767 | :param ips_on_flags: IPS stops, only valid for heated systems, will be None if not heated 768 | :param write: if True, write to disk, otherwise run matplotlib.pyplot.show() 769 | 770 | """ 771 | # # calculate mean power curve (mean of power curves from different directions), useful for plotting 772 | mpc = aepc.mean_power_curve(pc) 773 | if aepc.starttimestamp == datetime.datetime.min: 774 | start_time = data[0,aepc.ts_index] 775 | else: 776 | start_time = aepc.starttimestamp 777 | if aepc.stoptimestamp == datetime.datetime.max: 778 | stop_time = data[-1,aepc.ts_index] 779 | else: 780 | stop_time = aepc.stoptimestamp 781 | data_period = (stop_time-start_time).total_seconds()/60.0/60.0 782 | reference_start = data[0,aepc.ts_index] 783 | reference_stop = data[-1,aepc.ts_index] 784 | reference_data_period = (reference_stop-reference_start).total_seconds()/60.0/60.0 785 | tmax_power = aepc.theoretical_output_power(data, pc) 786 | theoretical_production = aepc.calculate_production(tmax_power, 1) 787 | actual_production = aepc.calculate_production(tmax_power, 2) 788 | theoretical_production_sum = np.nansum(theoretical_production[:, 1]) 789 | actual_production_sum = np.nansum(actual_production[:, 1]) 790 | total_losses = theoretical_production_sum - actual_production_sum 791 | total_losses_perc = ((theoretical_production_sum - actual_production_sum)/theoretical_production_sum) * 100.0 792 | # check for empty 793 | if np.shape(alarm_timings) == (0,): 794 | icing_loss_production = 0.0 795 | icing_duration = 0.0 796 | else: 797 | icing_loss_production = np.nansum(alarm_timings[:, 2]) 798 | icing_duration = np.nansum(alarm_timings[:, 3]) 799 | icing_loss_perc = (icing_loss_production/actual_production_sum) * 100.0 800 | #check for empty array (no stops) 801 | if np.shape(stop_timings) == (0,): 802 | stop_losses = 0.0 803 | stop_duration = 0.0 804 | else: 805 | stop_losses = np.nansum(stop_timings[:, 2]) 806 | stop_duration = np.nansum(stop_timings[:, 3]) 807 | stop_loss_perc = (stop_losses/actual_production_sum) * 100.0 808 | icing_duration_perc = (icing_duration / data_period) * 100.0 809 | stop_duration_perc = (stop_duration / data_period) * 100.0 810 | # check for empty 811 | if np.shape(over_timings) == (0,): 812 | over_prod_duration = 0.0 813 | else: 814 | over_prod_duration = np.nansum(over_timings[:, 3]) 815 | over_prod_duration_perc = (over_prod_duration / data_period) * 100.0 816 | availability = aepc.count_availability(data) * 100.0 817 | 818 | data_loss = ((data_sizes[0]-data_sizes[1])/data_sizes[0]) * 100.0 819 | reference_loss = ((data_sizes[0]-data_sizes[2])/data_sizes[0]) * 100.0 820 | # few exmaple plots 821 | 822 | ## scatterplot the data overlay the power curve 823 | # plot wind speed on x-axis and power on y-axis 824 | production_loss_label = "Lost production due to icing: {0:.1f} %".format(icing_loss_perc) 825 | stop_label = "Stops due to icing: {0:.1f} %".format(stop_loss_perc) 826 | overprod_label = "Overproduction: {0:.1f} % of total time".format(over_prod_duration_perc) 827 | 828 | matplotlib.rcParams.update({'font.size': 22}) 829 | # plt.style.use('bmh') 830 | fig0 = plt.figure(0) 831 | ax = fig0.gca() 832 | ax.plot(red_power[:, 2], red_power[:, 5], 'bo',label='standard production', alpha=0.5, markersize=6) 833 | # mark all cases where an alarm has been triggered with a red 'x' 834 | ax.plot(red_power[red_power[:,1] == 1.0, 2], red_power[red_power[:,1] == 1.0, 5], 'ro', label=production_loss_label, alpha=0.5, markersize=6) 835 | ax.plot(stops[stops[:,1] == 2.0, 2], stops[stops[:,1] == 2.0, 5], 'ko', label=stop_label, alpha=0.5, markersize=6) 836 | ax.plot(overprod[overprod[:,1] == 3.0, 2], overprod[overprod[:,1] == 3.0, 5], 'go', label=overprod_label, alpha=0.5, markersize=6) 837 | if ips_on_flags is not None: 838 | ips_label = "IPS ON" 839 | ax.plot(ips_on_flags[ips_on_flags[:, 1] != 0.0, 2], ips_on_flags[ips_on_flags[:, 1] != 0.0, 5], 'yo', label=ips_label, alpha=0.5, markersize=6) 840 | 841 | # plot a mean power curve and the P10 curve on top of the data 842 | ax.plot(mpc[:,0], mpc[:,2], 'c-', lw=4, label='Power curve') # linewidth 2 843 | ax.plot(mpc[:,0], mpc[:,3], 'c--', lw=4, label='P10') 844 | ax.plot(mpc[:,0], mpc[:,4], 'c-.', lw=4, label='P90') 845 | ax.set_title('Dataset: {0}\n start time: {1}, stop time: {2} \n data availability: {3:.1f}' 846 | .format(aepc.id,start_time.strftime("%Y-%m-%d %H:%M:%S"),stop_time.strftime("%Y-%m-%d %H:%M:%S"),availability)) 847 | ax.set_xlabel('Wind speed [m/s]') 848 | ax.set_ylabel('Power [kW]') 849 | tick_size = 2 850 | ax.set_xticks((list(range(0, self.power_curve_plot_max + tick_size, tick_size)))) 851 | ax.set_xlim((0, self.power_curve_plot_max)) 852 | ax.legend(loc='upper left', framealpha=0.3) 853 | # plt.show() 854 | # hide yaxis to obfuscate the true power values 855 | # ax = plt.gca() 856 | ax.axes.get_yaxis().set_visible(False) 857 | 858 | # # # plot the timeseries flag the ice cases 859 | # fig1 = plt.figure(1) 860 | # plt.title('Dataset: {0}, start time: {1}, stop time: {2}, data availability: {3:.1f}' 861 | # .format(aepc.id,start_time.strftime("%Y-%m-%d %H:%M:%S"),stop_time.strftime("%Y-%m-%d %H:%M:%S"),availability)) 862 | # # # plot the original timeseries on green 863 | # plt.plot_date(data[:, 0], data[:, aepc.pow_index], 'g-', label='observed power') 864 | # # # plot the reference data on black (this could also be red_power[:,3], its pretty much the same data) 865 | # plt.plot_date(stops[:, 0], stops[:, 3], 'k-', label='reference data') 866 | # # # mark all stops with red 867 | # plt.plot_date(stops[stops[:, 1] == 2.0, 0], stops[stops[:, 1] == 2.0, 5], 'r.', label='stops') 868 | # # # mark all cases where production has gone down with blue 869 | # plt.plot_date(red_power[red_power[:,1] == 1.0, 0], red_power[red_power[:,1] == 1.0, 5], 'b.', label='production loss') 870 | # plt.legend(loc='best') 871 | 872 | 873 | 874 | if write: 875 | pc_filename = aepc.result_dir +aepc.id + '_pc.png' 876 | fig0.set_size_inches(18.5, 10.5) 877 | fig0.savefig(pc_filename, bbox_inches='tight', dpi=300) 878 | # ts_filename = aepc.result_dir + aepc.id + '_ts.png' 879 | # fig1.set_size_inches(18.5, 10.5) 880 | # fig1.savefig(ts_filename, bbox_inches='tight', dpi=300) 881 | plt.close(0) 882 | # plt.close(1) 883 | print("{0} : Power curve plots written to : {1}" 884 | .format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), pc_filename)) 885 | else: 886 | plt.show() 887 | #============================================================================== 888 | # # # plot all directional power curves in one figure 889 | # plt.figure(2) 890 | # for i in range(len(aepc.direction_bins)): 891 | # lbl = "{0}".format(aepc.direction_bins[i]) 892 | # plt.plot(pc[:,i,0],pc[:,i,2],'-',label=lbl) 893 | # plt.plot(pc[:,i,0],pc[:,i,3],'--',label=lbl) 894 | # # plt.plot(pc[:,i,0],pc[:,i,4],'.-',label=lbl) 895 | # plt.legend() 896 | # plt.show() 897 | #============================================================================== 898 | 899 | def read_powercurve_from_file(self,filename): 900 | """ 901 | read powercurve from file produced by the program 902 | 903 | Some information is lost during the save process right now. real bin centers are not saved and neither are sample counts 904 | TODO: Needs to be updated, writing as well. Power curve now has a lot more information. 905 | might make sense to change the formatting of the power curve file to something a bit more structured 906 | :param filename: filename where the power curve sits 907 | :returns: power curve structure formatted in same way as earlier 908 | """ 909 | pc_file = open(filename,'r') 910 | line = next(pc_file) # otsikko 911 | line = next(pc_file) # suuntabinit 912 | dir_bins = [float(item.strip()) for item in line.split('\t')[1:]] 913 | pc = [] 914 | p10 = [] 915 | p90 = [] 916 | wind_bins = [] 917 | line = next(pc_file) # eka rivi 918 | while 'P10' not in line: 919 | try: 920 | wind_bins.append(float(line.split('\t')[0].strip())) 921 | pc.append([float(item.strip()) for item in line.split('\t')[1:]]) 922 | line = next(pc_file) 923 | except ValueError: 924 | break 925 | line = next(pc_file) # otsikko 926 | line = next(pc_file) # suuntabinit 927 | while 'P90' not in line: 928 | try: 929 | p10.append([float(item.strip()) for item in line.split('\t')[1:]]) 930 | line = next(pc_file) 931 | except ValueError: 932 | break 933 | line = next(pc_file) # otsikko 934 | line = next(pc_file) # suuntabinit 935 | while True: 936 | try: 937 | p90.append([float(item.strip()) for item in line.split('\t')[1:]]) 938 | line = next(pc_file) 939 | except StopIteration: 940 | break 941 | except ValueError: 942 | break 943 | 944 | full_pc = np.zeros((len(wind_bins),len(dir_bins),6)) 945 | for i in range(len(wind_bins)): # rivit 946 | for j in range(len(dir_bins)): #sarakkeet 947 | full_pc[i,j,0] = wind_bins[i] 948 | full_pc[i,j,1] = dir_bins[j] 949 | full_pc[i,j,2] = pc[i][j] 950 | full_pc[i,j,3] = p10[i][j] 951 | full_pc[i,j,4] = p90[i][j] 952 | pc_file.close() 953 | return full_pc 954 | 955 | 956 | 957 | --------------------------------------------------------------------------------