diff --git a/README.md b/README.md index 4aa5bf9cf..3e410e0cc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ FLORIS is a controls-focused wind farm simulation software incorporating steady-state engineering wake models into a performance-focused Python framework. It has been in active development at NREL since 2013 and the latest -release is [FLORIS v3.4.1](https://github.com/NREL/floris/releases/latest). +release is [FLORIS v3.5](https://github.com/NREL/floris/releases/latest). Online documentation is available at https://nrel.github.io/floris. The software is in active development and engagement with the development team @@ -71,7 +71,7 @@ and importing FLORIS: version VERSION - 3.4 + 3.5 FILE ~/floris/floris/__init__.py diff --git a/docs/architecture.md b/docs/architecture.md index 88da05b0e..682aa5c8b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,17 +1,29 @@ # Architecture and Design -Two fundamental ideas define the design of the FLORIS software: +At the outset of the design of the FLORIS software, a few fundamental ideas were identified +that should continue to guide future design decisions. These characteristics should never +be violated, and ongoing work should strive to meet these ideas and expand on them as much +as possible. -- Modularity in wake model formulation - - Mathematical formulation should be straightforward to include +- Modularity in wake model formulation: + - New mathematical formulation should be straightforward to incorporate. - Requisite solver and grid data structures should not conflict with other existing - wake models -- Management of abstraction + wake models. +- Any new feature or work should not affect an existing feature: + - Low level code should be reused as much as possible, but high level code should rarely + be repurposed. + - It is expected that a new feature will include a new user entry point + at the highest level. + - Avoid flags and if-statements that allow using one high-level routine for multiple unrelated + tasks. + - When in doubt, create a new pipeline from the user-level API to the low level implementation + and refactor to consolidate, if necessary, afterwards. +- Management of abstraction: - Low level code is opaque but well tested and exercised; it should be very computationally - efficient with low algorithmic complexity + efficient with low algorithmic complexity. - High level code should be expressive and clear even if it results in verbose or less - efficient code + efficient code. The FLORIS software consists of two primary high-level packages and a few other low level packages. The internal structure and hierarchy is described below. diff --git a/docs/code_quality.ipynb b/docs/code_quality.ipynb index 24c5353a1..d6a4322ee 100644 --- a/docs/code_quality.ipynb +++ b/docs/code_quality.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -62,47 +63,66 @@ "data = [\n", " (\"df25a9cfacd3d652361d2bd37f568af00acb2631\", \"df25a9cf\", datetime(2021, 12, 29), 1.2691, 1.2584, 1.6432, None, 0.4344, \"df25a9cf\"),\n", " (\"b797390a43298a815f3ff57955cfdc71ecf3e866\", \"b797390a\", datetime(2022, 1, 3), 0.6867, 1.2354, 1.8026, None, 0.2993, \"b797390a\"),\n", - " (\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\", \"01a02d5f\", datetime(2022, 1, 4), 0.4048, 0.8909, 1.4921, None, 0.3022, \"01a02d5f\"),\n", - " (\"dd847210082035d43b0273ae63a76a53cb8d2e12\", \"dd847210\", datetime(2022, 1, 6), 0.4004, 0.8622, 1.4506, None, 0.3627, \"dd847210\"),\n", - " (\"33779269e98cc882a5f066c462d8ec1eadf37a1a\", \"33779269\", datetime(2022, 1, 10), 0.4025, 0.8954, 1.5164, None, 0.3690, \"33779269\"),\n", - " (\"12890e029a7155b074b9b325d320d1798338e287\", \"12890e02\", datetime(2022, 1, 11), 0.3979, 0.9134, 1.5469, None, 0.3682, \"12890e02\"),\n", - " (\"66dafc08bd620d96deda7d526b0e4bfc3b086650\", \"66dafc08\", datetime(2022, 1, 12), 0.4175, 0.8834, 1.5187, None, 0.3709, \"66dafc08\"),\n", - " (\"a325819b3b03b84bd76ad455e3f9b4600744ba14\", \"a325819b\", datetime(2022, 1, 13), 0.4207, 0.8781, 1.5001, None, 0.3709, \"a325819b\"),\n", - " (\"8a2c1a610295c007f0222ce737723c341189811d\", \"8a2c1a61\", datetime(2022, 1, 14), 0.4108, 0.8914, 1.5599, None, 0.3708, \"8a2c1a61\"),\n", - " (\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\", \"c6bc79b0\", datetime(2022, 1, 14), 0.4172, 0.8813, 1.4888, None, 0.3701, \"c6bc79b0\"),\n", - " (\"03e1f461c152e4f221fe92c834f2787680cf5772\", \"03e1f461\", datetime(2022, 1, 18), 0.4294, 0.8760, 1.5124, 1.8728, 0.3673, \"PR #56\"),\n", - " (\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\", \"9e96d6c4\", datetime(2022, 1, 19), 0.4389, 0.8505, 1.4700, 1.8529, 0.3825, \"v3.0rc1\"),\n", - " (\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\", \"2a98428f\", datetime(2022, 2, 15), 0.2548, 0.8753, 1.5254, 1.8375, 0.3824, \"PR #317\"),\n", - " (\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\", \"9b4e85cf\", datetime(2022, 3, 1), 0.2687, 0.9676, 1.5895, 1.8790, 0.1572, \"v3.0\"),\n", - " (\"d18f4d263ecabf502242592f9d60815a07c7b89c\", \"d18f4d26\", datetime(2022, 3, 4), 0.2742, 0.9402, 1.5835, 1.8870, 0.1572, \"v3.0.1\"),\n", - " (\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\", \"a23241bb\", datetime(2022, 4, 6), 0.2609, 0.9793, 1.6281, 1.8673, 0.1682, \"v3.1\"),\n", - " (\"c2006b0011a5df036c306c15e75763ec492dafda\", \"c2006b00\", datetime(2022, 6, 22), 0.2733, 0.9668, 1.6002, 1.8838, 0.1681, \"v3.1.1\"),\n", - " (\"0c2adf3e702b6427da946a6ba9dbedbea22738be\", \"0c2adf3e\", datetime(2022, 9, 16), 0.2727, 0.9613, 1.5977, 1.8369, 0.1502, \"v3.2\"),\n", - " (\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\", \"39c46600\", datetime(2022, 11, 20), 0.2729, 0.9561, 1.5817, 1.8541, 0.1899, \"v3.2.1\"),\n", - " (\"8436fd78b002e5792f5d0dd1409332d171036d49\", \"8436fd78\", datetime(2023, 2, 8), 0.2753, 0.9718, 1.5985, 1.8721, 0.1905, \"v3.2.2\"),\n", - " (\"07a45b66c5facfea06c40bd82e34040c97560640\", \"07a45b66\", datetime(2023, 2, 8), 0.2763, 0.9837, 1.5750, 1.8805, 0.1972, \"07a45b66\"),\n", - " (\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\", \"1d84538c\", datetime(2023, 2, 22), 0.2747, 0.9457, 1.5743, 1.8628, 0.0000, \"1d84538c\"),\n", - " (\"4d528a3d6456621a382d409b5145a877b5414b88\", \"4d528a3d\", datetime(2023, 2, 23), 0.2669, 0.9502, 1.5503, 1.8683, 0.0000, \"4d528a3d\"),\n", - " (\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\", \"8c637b36\", datetime(2023, 2, 27), 0.2918, 0.9974, 1.5609, 1.8825, 0.0000, \"8c637b36\"),\n", - " (\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\", \"4d23fa6d\", datetime(2023, 2, 27), 0.2962, 0.9924, 1.5983, 1.8535, 0.0000, \"4d23fa6d\"),\n", - " (\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\", \"015f6874\", datetime(2023, 3, 1), 0.2990, 1.0068, 1.5856, 1.8722, 0.0000, \"015f6874\"),\n", - " (\"26f06d449da208ce64724b1463b07ad20746cbdc\", \"26f06d44\", datetime(2023, 3, 6), 0.2701, 0.9652, 1.5992, 1.8506, 0.0000, \"26f06d44\"),\n", - " (\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\", \"6b9d6bb8\", datetime(2023, 3, 6), 0.2964, 0.9775, 1.6261, 1.8816, 0.0000, \"6b9d6bb8\"),\n", - " (\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\", \"b796bd0f\", datetime(2023, 3, 6), 0.2692, 0.9455, 1.5827, 1.8598, 0.0000, \"b796bd0f\"),\n", - " (\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\", \"780aef7c\", datetime(2023, 3, 7), 0.2980, 0.9909, 1.5796, 1.8696, 0.0000, \"780aef7c\"),\n", - " (\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\", \"9f93ad9b\", datetime(2023, 3, 7), 0.2985, 0.9925, 1.5896, 1.8813, 0.0000, \"9f93ad9b\"),\n", - " (\"16628a0ba45a675df762245694e0a7666a3478f8\", \"16628a0b\", datetime(2023, 3, 7), 0.3013, 0.9700, 1.5791, 1.8950, 0.1972, \"v3.3\"),\n", - " (\"01684c8559604344bd09791268131819a09770a8\", \"01684c85\", datetime(2023, 3, 17), 0.3016, 0.9931, 1.5986, 1.8960, 0.0000, \"01684c85\"),\n", - " (\"e9231fb893c765b723fa4c1e087a58761b6aa471\", \"e9231fb8\", datetime(2023, 3, 20), 0.2974, 0.9963, 1.5817, 1.8798, 0.0000, \"e9231fb8\"),\n", - " (\"219889e243ffc69c71b6f7747f5af751d5694de1\", \"219889e2\", datetime(2023, 3, 23), 0.2897, 1.0008, 1.5651, 1.8983, 0.0000, \"219889e2\"),\n", - " (\"6124d2a82a7a823722210bc2e8516d355ba19eb3\", \"6124d2a8\", datetime(2023, 4, 5), 0.2971, 0.9918, 1.5904, 1.9332, 0.0000, \"6124d2a8\"),\n", - " (\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\", \"f6e4287f\", datetime(2023, 4, 25), 0.3045, 0.9905, 1.6114, 1.8999, 0.0000, \"f6e4287f\"),\n", - " (\"f2797fef396f2f19b02abb1f9555b678dac614f1\", \"f2797fef\", datetime(2023, 4, 25), 0.3071, 1.0112, 1.5760, 1.8921, 0.0000, \"f2797fef\"),\n", - " (\"b4e538f530048fec58eaca5170be82c67dbdcceb\", \"b4e538f5\", datetime(2023, 4, 25), 0.2924, 0.9751, 1.6105, 1.9043, 0.0000, \"b4e538f5\"),\n", - " (\"68820b715ed6b2c981aa11d29c0102e879280d79\", \"68820b71\", datetime(2023, 4, 25), 0.3013, 0.9936, 1.6038, 1.9069, 0.0000, \"68820b71\"),\n", - " (\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\", \"03deffed\", datetime(2023, 4, 25), 0.2930, 0.9882, 1.6013, 1.9015, 0.0000, \"03deffed\"),\n", - " (\"0d2bfecc271d561f67050659684b4797af8ee740\", \"0d2bfecc\", datetime(2023, 4, 25), 0.3041, 1.0009, 1.5853, 1.8890, 0.0000, \"0d2bfecc\"),\n", - " (\"1d03a465593f56c99a64a576d185d4ed17b659f2\", \"1d03a465\", datetime(2023, 4, 25), 0.3058, 0.9970, 1.5849, 1.8224, 0.0000, \"1d03a465\"),\n", + " (\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\", \"01a02d5f\", datetime(2022, 1, 4), 0.3697, 0.8080, 1.3633, None, 0.3022, \"01a02d5f\"),\n", + " (\"dd847210082035d43b0273ae63a76a53cb8d2e12\", \"dd847210\", datetime(2022, 1, 6), 0.3551, 0.8127, 1.3390, None, 0.3627, \"dd847210\"),\n", + " (\"33779269e98cc882a5f066c462d8ec1eadf37a1a\", \"33779269\", datetime(2022, 1, 10), 0.3544, 0.8298, 1.4116, None, 0.3690, \"33779269\"),\n", + " (\"12890e029a7155b074b9b325d320d1798338e287\", \"12890e02\", datetime(2022, 1, 11), 0.3511, 0.8392, 1.3829, None, 0.3682, \"12890e02\"),\n", + " (\"66dafc08bd620d96deda7d526b0e4bfc3b086650\", \"66dafc08\", datetime(2022, 1, 12), 0.3807, 0.8212, 1.3877, None, 0.3709, \"66dafc08\"),\n", + " (\"a325819b3b03b84bd76ad455e3f9b4600744ba14\", \"a325819b\", datetime(2022, 1, 13), 0.3741, 0.8185, 1.3809, None, 0.3709, \"a325819b\"),\n", + " (\"8a2c1a610295c007f0222ce737723c341189811d\", \"8a2c1a61\", datetime(2022, 1, 14), 0.3689, 0.8106, 1.4001, None, 0.3708, \"8a2c1a61\"),\n", + " (\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\", \"c6bc79b0\", datetime(2022, 1, 14), 0.3652, 0.8013, 1.4334, None, 0.3701, \"c6bc79b0\"),\n", + " (\"03e1f461c152e4f221fe92c834f2787680cf5772\", \"03e1f461\", datetime(2022, 1, 18), 0.3688, 0.7887, 1.4096, 1.5801, 0.3673, \"PR #56\"),\n", + " (\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\", \"9e96d6c4\", datetime(2022, 1, 19), 0.3807, 0.7835, 1.3760, 1.5815, 0.3825, \"v3.0rc1\"),\n", + " (\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\", \"2a98428f\", datetime(2022, 2, 15), 0.2362, 0.7966, 1.3459, 1.5618, 0.3824, \"PR #317\"),\n", + " (\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\", \"9b4e85cf\", datetime(2022, 3, 1), 0.2479, 0.8836, 1.4280, 1.6094, 0.1572, \"v3.0\"),\n", + " (\"d18f4d263ecabf502242592f9d60815a07c7b89c\", \"d18f4d26\", datetime(2022, 3, 4), 0.2471, 0.8861, 1.4601, 1.5769, 0.1572, \"v3.0.1\"),\n", + " (\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\", \"a23241bb\", datetime(2022, 4, 6), 0.2450, 0.8902, 1.4467, 1.5751, 0.1682, \"v3.1\"),\n", + " (\"c2006b0011a5df036c306c15e75763ec492dafda\", \"c2006b00\", datetime(2022, 6, 22), 0.2507, 0.8754, 1.4494, 1.6140, 0.1681, \"v3.1.1\"),\n", + " (\"0c2adf3e702b6427da946a6ba9dbedbea22738be\", \"0c2adf3e\", datetime(2022, 9, 16), 0.2524, 0.8878, 1.4293, 1.5771, 0.1502, \"v3.2\"),\n", + " (\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\", \"39c46600\", datetime(2022, 11, 20), 0.2521, 0.9094, 1.4539, 1.5681, 0.1899, \"v3.2.1\"),\n", + " (\"8436fd78b002e5792f5d0dd1409332d171036d49\", \"8436fd78\", datetime(2023, 2, 8), 0.2583, 0.8718, 1.4540, 1.5907, 0.1905, \"v3.2.2\"),\n", + " (\"07a45b66c5facfea06c40bd82e34040c97560640\", \"07a45b66\", datetime(2023, 2, 8), 0.2486, 0.8641, 1.4610, 1.6280, 0.1972, \"07a45b66\"),\n", + " (\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\", \"1d84538c\", datetime(2023, 2, 22), 0.2515, 0.8864, 1.4248, 1.5819, 0.0000, \"1d84538c\"),\n", + " (\"4d528a3d6456621a382d409b5145a877b5414b88\", \"4d528a3d\", datetime(2023, 2, 23), 0.2557, 0.8969, 1.4104, 1.5799, 0.0000, \"4d528a3d\"),\n", + " (\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\", \"8c637b36\", datetime(2023, 2, 27), 0.2773, 0.9240, 1.4645, 1.5997, 0.0000, \"8c637b36\"),\n", + " (\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\", \"4d23fa6d\", datetime(2023, 2, 27), 0.2753, 0.9019, 1.4714, 1.6053, 0.0000, \"4d23fa6d\"),\n", + " (\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\", \"015f6874\", datetime(2023, 3, 1), 0.2817, 0.9160, 1.5027, 1.6109, 0.0000, \"015f6874\"),\n", + " (\"26f06d449da208ce64724b1463b07ad20746cbdc\", \"26f06d44\", datetime(2023, 3, 6), 0.2556, 0.9089, 1.4561, 1.5715, 0.0000, \"26f06d44\"),\n", + " (\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\", \"6b9d6bb8\", datetime(2023, 3, 6), 0.2790, 0.8965, 1.4669, 1.6271, 0.0000, \"6b9d6bb8\"),\n", + " (\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\", \"b796bd0f\", datetime(2023, 3, 6), 0.2512, 0.8887, 1.4229, 1.5932, 0.0000, \"b796bd0f\"),\n", + " (\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\", \"780aef7c\", datetime(2023, 3, 7), 0.2794, 0.9088, 1.4623, 1.6361, 0.0000, \"780aef7c\"),\n", + " (\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\", \"9f93ad9b\", datetime(2023, 3, 7), 0.2768, 0.9222, 1.4693, 1.5947, 0.0000, \"9f93ad9b\"),\n", + " (\"16628a0ba45a675df762245694e0a7666a3478f8\", \"16628a0b\", datetime(2023, 3, 7), 0.2711, 0.9046, 1.4684, 1.6177, 0.1972, \"v3.3\"),\n", + " (\"01684c8559604344bd09791268131819a09770a8\", \"01684c85\", datetime(2023, 3, 17), 0.2780, 0.8966, 1.4665, 1.6184, 0.0000, \"01684c85\"),\n", + " (\"e9231fb893c765b723fa4c1e087a58761b6aa471\", \"e9231fb8\", datetime(2023, 3, 20), 0.2766, 0.9115, 1.4520, 1.6247, 0.0000, \"e9231fb8\"),\n", + " (\"219889e243ffc69c71b6f7747f5af751d5694de1\", \"219889e2\", datetime(2023, 3, 23), 0.2789, 0.9155, 1.4392, 1.6172, 0.0000, \"219889e2\"),\n", + " (\"6124d2a82a7a823722210bc2e8516d355ba19eb3\", \"6124d2a8\", datetime(2023, 4, 5), 0.2793, 0.9102, 1.4421, 1.5822, 0.0000, \"6124d2a8\"),\n", + " (\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\", \"f6e4287f\", datetime(2023, 4, 25), 0.2811, 0.9127, 1.4950, 1.6169, 0.0000, \"f6e4287f\"),\n", + " (\"f2797fef396f2f19b02abb1f9555b678dac614f1\", \"f2797fef\", datetime(2023, 4, 25), 0.2821, 0.9147, 1.4608, 1.5963, 0.0000, \"f2797fef\"),\n", + " (\"b4e538f530048fec58eaca5170be82c67dbdcceb\", \"b4e538f5\", datetime(2023, 4, 25), 0.2819, 0.9356, 1.4703, 1.6130, 0.0000, \"b4e538f5\"),\n", + " (\"68820b715ed6b2c981aa11d29c0102e879280d79\", \"68820b71\", datetime(2023, 4, 25), 0.2761, 0.9253, 1.4697, 1.6078, 0.0000, \"68820b71\"),\n", + " (\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\", \"03deffed\", datetime(2023, 4, 25), 0.2842, 0.9258, 1.4871, 1.6294, 0.0000, \"03deffed\"),\n", + " (\"0d2bfecc271d561f67050659684b4797af8ee740\", \"0d2bfecc\", datetime(2023, 4, 25), 0.2778, 0.9204, 1.4512, 1.6204, 0.0000, \"0d2bfecc\"),\n", + " (\"1d03a465593f56c99a64a576d185d4ed17b659f2\", \"1d03a465\", datetime(2023, 4, 25), 0.2814, 0.8922, 1.4347, 1.6041, 0.0000, \"1d03a465\"),\n", + " (\"78a953b7ef9a36b62e5b446c80ed68abfddbfb74\", \"78a953b7\", datetime(2023, 5, 4), 0.2840, 0.9124, 1.4620, 1.6255, 0.0000, \"78a953b7\"),\n", + " (\"6c4f70ffbf3d4d2922d41d0032ae1b93d8a23c99\", \"6c4f70ff\", datetime(2023, 5, 4), 0.2833, 0.9212, 1.4702, 1.6108, 0.0000, \"6c4f70ff\"),\n", + " (\"ab03282623d0262b20b8c132efcdcace2dace766\", \"ab032826\", datetime(2023, 5, 6), 0.2568, 0.8974, 1.3970, 1.5902, 0.0000, \"ab032826\"),\n", + " (\"d2f7a45af27a6b40027d6f6a0f4f0be0c6dee5d9\", \"d2f7a45a\", datetime(2023, 5, 6), 0.2576, 0.8952, 1.4196, 1.5975, 0.0000, \"d2f7a45a\"),\n", + " (\"98b23f3d517481b127f190f5f8b7ebfae7f8b6b2\", \"98b23f3d\", datetime(2023, 5, 6), 0.2595, 0.8805, 1.4311, 1.5829, 0.0000, \"98b23f3d\"),\n", + " (\"452425de723cc1640d999022389672caf9bffbd0\", \"452425de\", datetime(2023, 5, 6), 0.2604, 0.9168, 1.4529, 1.5993, 0.0000, \"452425de\"),\n", + " (\"85dadb1a566c9fa8dc84cb9837b98bd5d23b8d58\", \"85dadb1a\", datetime(2023, 5, 7), 0.2607, 0.8738, 1.4423, 1.5903, 0.0000, \"85dadb1a\"),\n", + " (\"432ee7f96c1f6cccd05a0034c86c720cdb63a3e6\", \"432ee7f9\", datetime(2023, 5, 10), 0.2595, 0.8962, 1.4363, 1.6035, 0.0000, \"432ee7f9\"),\n", + " (\"ebd70ecaef14c0e239337eb6e36506303378a31a\", \"ebd70eca\", datetime(2023, 5, 10), 0.2589, 0.8722, 1.4290, 1.5817, 0.0000, \"ebd70eca\"),\n", + " (\"77fa7155d55bdf3fd43e29f58fe57feffcb107cf\", \"77fa7155\", datetime(2023, 5, 11), 0.2601, 0.8918, 1.4060, 1.6026, 0.0000, \"77fa7155\"),\n", + " (\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\", \"d5d4b134\", datetime(2023, 5, 12), 0.2618, 0.8922, 1.4582, 1.6016, 0.0000, \"d5d4b134\"),\n", + " (\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\", \"d5d4b134\", datetime(2023, 5, 16), 0.2601, 0.9014, 1.4262, 1.5990, 0.0000, \"d5d4b134\"),\n", + " (\"7c879f1ce18b52d9b0a8eecf877d03e66afc975b\", \"7c879f1c\", datetime(2023, 5, 16), 0.2556, 0.9021, 1.4220, 1.5603, 0.0000, \"7c879f1c\"),\n", + " (\"2aa9f2a55686f2ee5dc407e8e0223eb25176d906\", \"2aa9f2a5\", datetime(2023, 5, 16), 0.2565, 0.8991, 1.4399, 1.5938, 0.0000, \"2aa9f2a5\"),\n", + " (\"5e5bb7f4e653621e7a81ff4bcaa27dbc1f759de7\", \"5e5bb7f4\", datetime(2023, 5, 16), 0.2545, 0.9005, 1.4188, 1.5943, 0.0000, \"v3.4\"),\n", + " (\"d91953a499dfb88b457a1e7a07903debbda4058b\", \"d91953a4\", datetime(2023, 6, 1), 0.2572, 0.8675, 1.4323, 1.5862, 0.0000, \"d91953a4\"),\n", + " (\"76742879c81c9baced49b9fc60abbf1d2eba65ff\", \"76742879\", datetime(2023, 7, 3), 0.2558, 0.8890, 1.4395, 1.5833, 0.0000, \"76742879\"),\n", + " (\"9c73a41eaca95bb718ac79980a1799dfa1c48cf3\", \"9c73a41e\", datetime(2023, 7, 6), 0.2608, 0.8788, 1.4301, 1.5938, 0.0000, \"9c73a41e\"),\n", + " (\"67104dd714de939be136646af68edd9643ddfcd3\", \"67104dd7\", datetime(2023, 7, 6), 0.3009, 0.8573, 1.0494, 1.2918, 0.0000, \"67104dd7\"),\n", "]\n", "\n", "df = pd.DataFrame(data=data, columns=columns)\n", @@ -122,7 +142,7 @@ "data": { "text/html": [ "\n", - "
\n" + "
\n" ] }, "metadata": {}, @@ -130,7 +150,7 @@ }, { "data": { - "application/javascript": "(function(root) {\n function embed_document(root) {\n const docs_json = {\"14a00e77-c0d8-428a-ada5-8c30bc2ee6af\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1016\"}],\"center\":[{\"id\":\"1019\"},{\"id\":\"1023\"},{\"id\":\"1068\"}],\"height\":450,\"left\":[{\"id\":\"1020\"}],\"renderers\":[{\"id\":\"1044\"},{\"id\":\"1074\"},{\"id\":\"1102\"},{\"id\":\"1131\"},{\"id\":\"1159\"},{\"id\":\"1188\"},{\"id\":\"1216\"},{\"id\":\"1245\"}],\"title\":{\"id\":\"1006\"},\"toolbar\":{\"id\":\"1032\"},\"x_range\":{\"id\":\"1008\"},\"x_scale\":{\"id\":\"1012\"},\"y_range\":{\"id\":\"1010\"},\"y_scale\":{\"id\":\"1014\"}},\"id\":\"1005\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{\"data\":{\"cc\":{\"__ndarray__\":\"AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H9JLv8h/fb9P2gibHh6pf0/ZmZmZmZm/T+q8dJNYhD+P/7UeOkmMf4//yH99nXg/T/cRgN4CyT+P8BbIEHxY/0/tTf4wmSq/T8ydy0hH/T9P0oMAiuHFv4/INJvXwfO/T9q3nGKjuT9Px+F61G4Hv4/Di2yne+n/T+jI7n8h/T9P0Ck374OnP0/JXUCmggb/j/gnBGlvcH9PyegibDh6f0/0m9fB84Z/j9SuB6F61H+P7x0kxgEVv4/MlUwKqkT/j/l8h/Sb1/+PwMJih9j7v4/9bnaiv1l/j+DL0ymCkb+P2Rd3EYDeP4/3+ALk6mC/j8GgZVDi2z+P9NNYhBYOf4/uK8D54wo/T8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"code_coverage\":{\"__ndarray__\":\"PnlYqDXN2z/WVuwvuyfTPxB6Nqs+V9M/vJaQD3o21z8EVg4tsp3XP+PHmLuWkNc/lIeFWtO81z+Uh4Va07zXP9DVVuwvu9c/cvkP6bev1z/9h/Tb14HXP3sUrkfhetg/t2J/2T152D+QMXctIR/EP5Axdy0hH8Q/K/aX3ZOHxT+jkjoBTYTFP0T67evAOcM/ylTBqKROyD/8qfHSTWLIP662Yn/ZPck/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACutmJ/2T3JPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"commit_hash\":[\"df25a9cfacd3d652361d2bd37f568af00acb2631\",\"b797390a43298a815f3ff57955cfdc71ecf3e866\",\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\",\"dd847210082035d43b0273ae63a76a53cb8d2e12\",\"33779269e98cc882a5f066c462d8ec1eadf37a1a\",\"12890e029a7155b074b9b325d320d1798338e287\",\"66dafc08bd620d96deda7d526b0e4bfc3b086650\",\"a325819b3b03b84bd76ad455e3f9b4600744ba14\",\"8a2c1a610295c007f0222ce737723c341189811d\",\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\",\"03e1f461c152e4f221fe92c834f2787680cf5772\",\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\",\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\",\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\",\"d18f4d263ecabf502242592f9d60815a07c7b89c\",\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\",\"c2006b0011a5df036c306c15e75763ec492dafda\",\"0c2adf3e702b6427da946a6ba9dbedbea22738be\",\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\",\"8436fd78b002e5792f5d0dd1409332d171036d49\",\"07a45b66c5facfea06c40bd82e34040c97560640\",\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\",\"4d528a3d6456621a382d409b5145a877b5414b88\",\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\",\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\",\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\",\"26f06d449da208ce64724b1463b07ad20746cbdc\",\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\",\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\",\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\",\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\",\"16628a0ba45a675df762245694e0a7666a3478f8\",\"01684c8559604344bd09791268131819a09770a8\",\"e9231fb893c765b723fa4c1e087a58761b6aa471\",\"219889e243ffc69c71b6f7747f5af751d5694de1\",\"6124d2a82a7a823722210bc2e8516d355ba19eb3\",\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\",\"f2797fef396f2f19b02abb1f9555b678dac614f1\",\"b4e538f530048fec58eaca5170be82c67dbdcceb\",\"68820b715ed6b2c981aa11d29c0102e879280d79\",\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\",\"0d2bfecc271d561f67050659684b4797af8ee740\",\"1d03a465593f56c99a64a576d185d4ed17b659f2\"],\"commit_hash_8char\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"03e1f461\",\"9e96d6c4\",\"2a98428f\",\"9b4e85cf\",\"d18f4d26\",\"a23241bb\",\"c2006b00\",\"0c2adf3e\",\"39c46600\",\"8436fd78\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"16628a0b\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\"],\"date\":{\"__ndarray__\":\"AACAyDfgd0IAAEDF0+F3QgAAACsm4ndCAACA9srid0IAAICNFOR3QgAAQPNm5HdCAAAAWbnkd0IAAMC+C+V3QgAAgCRe5XdCAACAJF7ld0IAAIC7p+Z3QgAAQCH65ndCAACA3Krvd0IAAABtLPR3QgAAQJ4j9XdCAAAAvML/d0IAAMBWixh4QgAAQIU5NHhCAAAAWyVJeEIAAAAn5WJ4QgAAACflYnhCAACAt2ZneEIAAEAduWd4QgAAQLQCaXhCAABAtAJpeEIAAMB/p2l4QgAAgHxDa3hCAACAfENreEIAAIB8Q2t4QgAAQOKVa3hCAABA4pVreEIAAEDilWt4QgAAwNvNbnhCAAAADcVveEIAAEA+vHB4QgAAAGnrdHhCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEI=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"gauss\":{\"__ndarray__\":\"GJXUCWgi9D+Hp1fKMsTzP240gLdAguw/845TdCSX6z8s1JrmHafsPyZTBaOSOu0/MCqpE9BE7D9hw9MrZRnsP9jw9EpZhuw/pN++Dpwz7D/VeOkmMQjsP57vp8ZLN+s/pgpGJXUC7D/YgXNGlPbuP9lfdk8eFu4/LSEf9GxW7z/HuriNBvDuPzSitDf4wu4/RpT2Bl+Y7j/wFkhQ/BjvPwpoImx4eu8/bHh6pSxD7j8rGJXUCWjuPwn5oGez6u8/4JwRpb3B7z8HzhlR2hvwP6UsQxzr4u4/SOF6FK5H7z+oxks3iUHuP6Fns+pzte8/w/UoXI/C7z8K16NwPQrvPw8LtaZ5x+8/UiegibDh7z+IY13cRgPwP5SHhVrTvO8/GQRWDi2y7z92cRsN4C3wPxWMSuoENO8/escpOpLL7z/IBz2bVZ/vP/kP6bevA/A/gZVDi2zn7z8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"gch\":{\"__ndarray__\":\"X5hMFYxK+j9IUPwYc9f8Px3J5T+k3/c/2T15WKg19z9seHqlLEP4Pxzr4jYawPg/lPYGX5hM+D9xrIvbaAD4P4V80LNZ9fg/io7k8h/S9z/ChqdXyjL4P4XrUbgehfc/KxiV1Alo+D8730+Nl275P7x0kxgEVvk/seHplbIM+j988rBQa5r5P3EbDeAtkPk/ylTBqKRO+T/6fmq8dJP5PzMzMzMzM/k/HHxhMlUw+T8g0m9fB874P+84RUdy+fg/GCZTBaOS+T8Cmggbnl75PxE2PL1Slvk/3GgAb4EE+j80ETY8vVL5P4MvTKYKRvk/rIvbaABv+T9O0ZFc/kP5P2sr9pfdk/k/ylTBqKRO+T97gy9Mpgr5PzXvOEVHcvk/8WPMXUvI+T+e76fGSzf5P/hT46WbxPk/YTJVMCqp+T9XW7G/7J75P6+UZYhjXfk/6+I2GsBb+T8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],\"jensen\":{\"__ndarray__\":\"WKg1zTtO9D/vOEVHcvnlP2PuWkI+6Nk/qmBUUieg2T/D9Shcj8LZP4EExY8xd9k/uB6F61G42j8/V1uxv+zaP1+YTBWMSto/bAn5oGez2j/swDkjSnvbP7u4jQbwFtw/ylTBqKRO0D9R2ht8YTLRP3gLJCh+jNE/irDh6ZWy0D+Sy39Iv33RP/mgZ7Pqc9E/gQTFjzF30T/mriXkg57RP5Cg+DHmrtE/TYQNT6+U0T+GWtO84xTRP1tCPujZrNI/FNBE2PD00j+JQWDl0CLTPwyTqYJRSdE/nDOitDf40j8mUwWjkjrRP99PjZduEtM/tMh2vp8a0z8qOpLLf0jTP3ZPHhZqTdM/RiV1ApoI0z9DrWnecYrSP/kP6bevA9M/sHJoke180z+dgCbChqfTP/RsVn2uttI/KjqSy39I0z+Nl24Sg8DSP5+rrdhfdtM/p3nHKTqS0z8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"tooltip_label\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"PR #56\",\"v3.0rc1\",\"PR #317\",\"v3.0\",\"v3.0.1\",\"v3.1\",\"v3.1.1\",\"v3.2\",\"v3.2.1\",\"v3.2.2\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"v3.3\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\"]},\"selected\":{\"id\":\"1054\"},\"selection_policy\":{\"id\":\"1053\"}},\"id\":\"1003\",\"type\":\"ColumnDataSource\"},{\"attributes\":{\"tools\":[{\"id\":\"1024\"},{\"id\":\"1025\"},{\"id\":\"1026\"},{\"id\":\"1027\"},{\"id\":\"1028\"},{\"id\":\"1029\"},{\"id\":\"1031\"}]},\"id\":\"1032\",\"type\":\"Toolbar\"},{\"attributes\":{\"fill_color\":{\"value\":\"blue\"},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1071\",\"type\":\"Circle\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1030\",\"type\":\"BoxAnnotation\"},{\"attributes\":{},\"id\":\"1028\",\"type\":\"ResetTool\"},{\"attributes\":{},\"id\":\"1027\",\"type\":\"SaveTool\"},{\"attributes\":{\"overlay\":{\"id\":\"1030\"}},\"id\":\"1026\",\"type\":\"BoxZoomTool\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1242\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1244\"},\"nonselection_glyph\":{\"id\":\"1243\"},\"view\":{\"id\":\"1246\"}},\"id\":\"1245\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"1024\",\"type\":\"PanTool\"},{\"attributes\":{},\"id\":\"1025\",\"type\":\"WheelZoomTool\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"cyan\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1244\",\"type\":\"Circle\"},{\"attributes\":{\"callback\":null,\"tooltips\":[[\"git ref\",\"@tooltip_label\"]]},\"id\":\"1031\",\"type\":\"HoverTool\"},{\"attributes\":{},\"id\":\"1021\",\"type\":\"BasicTicker\"},{\"attributes\":{\"axis\":{\"id\":\"1020\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1023\",\"type\":\"Grid\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1246\",\"type\":\"CDSView\"},{\"attributes\":{\"axis_label\":\"Time to solution (s)\",\"coordinates\":null,\"formatter\":{\"id\":\"1048\"},\"group\":null,\"major_label_policy\":{\"id\":\"1049\"},\"ticker\":{\"id\":\"1021\"}},\"id\":\"1020\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"1029\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1014\",\"type\":\"LinearScale\"},{\"attributes\":{\"axis\":{\"id\":\"1016\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1019\",\"type\":\"Grid\"},{\"attributes\":{\"fill_color\":{\"value\":\"red\"},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1185\",\"type\":\"Circle\"},{\"attributes\":{\"num_minor_ticks\":5,\"tickers\":[{\"id\":\"1056\"},{\"id\":\"1057\"},{\"id\":\"1058\"},{\"id\":\"1059\"},{\"id\":\"1060\"},{\"id\":\"1061\"},{\"id\":\"1062\"},{\"id\":\"1063\"},{\"id\":\"1064\"},{\"id\":\"1065\"},{\"id\":\"1066\"},{\"id\":\"1067\"}]},\"id\":\"1017\",\"type\":\"DatetimeTicker\"},{\"attributes\":{},\"id\":\"1054\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"1053\",\"type\":\"UnionRenderers\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1214\",\"type\":\"Line\"},{\"attributes\":{\"label\":{\"value\":\"cc\"},\"renderers\":[{\"id\":\"1216\"},{\"id\":\"1245\"}]},\"id\":\"1240\",\"type\":\"LegendItem\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1215\",\"type\":\"Line\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"cyan\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1243\",\"type\":\"Circle\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1217\",\"type\":\"CDSView\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"red\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1187\",\"type\":\"Circle\"},{\"attributes\":{\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1213\",\"type\":\"Line\"},{\"attributes\":{\"fill_color\":{\"value\":\"cyan\"},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1242\",\"type\":\"Circle\"},{\"attributes\":{\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1041\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1048\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1213\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1215\"},\"nonselection_glyph\":{\"id\":\"1214\"},\"view\":{\"id\":\"1217\"}},\"id\":\"1216\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1158\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1043\",\"type\":\"Line\"},{\"attributes\":{\"border_line_color\":\"black\",\"click_policy\":\"mute\",\"coordinates\":null,\"group\":null,\"items\":[{\"id\":\"1069\"},{\"id\":\"1126\"},{\"id\":\"1183\"},{\"id\":\"1240\"}],\"location\":\"bottom_left\"},\"id\":\"1068\",\"type\":\"Legend\"},{\"attributes\":{\"label\":{\"value\":\"gch\"},\"renderers\":[{\"id\":\"1159\"},{\"id\":\"1188\"}]},\"id\":\"1183\",\"type\":\"LegendItem\"},{\"attributes\":{\"label\":{\"value\":\"jensen\"},\"renderers\":[{\"id\":\"1044\"},{\"id\":\"1074\"}]},\"id\":\"1069\",\"type\":\"LegendItem\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1042\",\"type\":\"Line\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1045\",\"type\":\"CDSView\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1132\",\"type\":\"CDSView\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1189\",\"type\":\"CDSView\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1041\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1043\"},\"nonselection_glyph\":{\"id\":\"1042\"},\"view\":{\"id\":\"1045\"}},\"id\":\"1044\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"green\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1130\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1010\",\"type\":\"DataRange1d\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1157\",\"type\":\"Line\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1128\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1130\"},\"nonselection_glyph\":{\"id\":\"1129\"},\"view\":{\"id\":\"1132\"}},\"id\":\"1131\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"1008\",\"type\":\"DataRange1d\"},{\"attributes\":{\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1156\",\"type\":\"Line\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1185\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1187\"},\"nonselection_glyph\":{\"id\":\"1186\"},\"view\":{\"id\":\"1189\"}},\"id\":\"1188\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"red\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1186\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1012\",\"type\":\"LinearScale\"},{\"attributes\":{\"coordinates\":null,\"group\":null,\"text\":\"5x5 Wind Farm Timing Test\"},\"id\":\"1006\",\"type\":\"Title\"},{\"attributes\":{\"axis_label\":\"Commit date\",\"coordinates\":null,\"formatter\":{\"id\":\"1051\"},\"group\":null,\"major_label_policy\":{\"id\":\"1052\"},\"ticker\":{\"id\":\"1017\"}},\"id\":\"1016\",\"type\":\"DatetimeAxis\"},{\"attributes\":{},\"id\":\"1051\",\"type\":\"DatetimeTickFormatter\"},{\"attributes\":{},\"id\":\"1049\",\"type\":\"AllLabels\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1156\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1158\"},\"nonselection_glyph\":{\"id\":\"1157\"},\"view\":{\"id\":\"1160\"}},\"id\":\"1159\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"1052\",\"type\":\"AllLabels\"},{\"attributes\":{\"mantissas\":[1,2,5],\"max_interval\":500.0,\"num_minor_ticks\":0},\"id\":\"1056\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"label\":{\"value\":\"gauss\"},\"renderers\":[{\"id\":\"1102\"},{\"id\":\"1131\"}]},\"id\":\"1126\",\"type\":\"LegendItem\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1101\",\"type\":\"Line\"},{\"attributes\":{\"days\":[1,8,15,22]},\"id\":\"1061\",\"type\":\"DaysTicker\"},{\"attributes\":{\"base\":60,\"mantissas\":[1,2,5,10,15,20,30],\"max_interval\":1800000.0,\"min_interval\":1000.0,\"num_minor_ticks\":0},\"id\":\"1057\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1100\",\"type\":\"Line\"},{\"attributes\":{\"base\":24,\"mantissas\":[1,2,4,6,8,12],\"max_interval\":43200000.0,\"min_interval\":3600000.0,\"num_minor_ticks\":0},\"id\":\"1058\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{},\"id\":\"1067\",\"type\":\"YearsTicker\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1160\",\"type\":\"CDSView\"},{\"attributes\":{\"days\":[1,4,7,10,13,16,19,22,25,28]},\"id\":\"1060\",\"type\":\"DaysTicker\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"green\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1129\",\"type\":\"Circle\"},{\"attributes\":{\"days\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]},\"id\":\"1059\",\"type\":\"DaysTicker\"},{\"attributes\":{\"months\":[0,6]},\"id\":\"1066\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"days\":[1,15]},\"id\":\"1062\",\"type\":\"DaysTicker\"},{\"attributes\":{\"months\":[0,1,2,3,4,5,6,7,8,9,10,11]},\"id\":\"1063\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"months\":[0,2,4,6,8,10]},\"id\":\"1064\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"months\":[0,4,8]},\"id\":\"1065\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1073\",\"type\":\"Circle\"},{\"attributes\":{\"fill_color\":{\"value\":\"green\"},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1128\",\"type\":\"Circle\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1075\",\"type\":\"CDSView\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1103\",\"type\":\"CDSView\"},{\"attributes\":{\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1099\",\"type\":\"Line\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1071\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1073\"},\"nonselection_glyph\":{\"id\":\"1072\"},\"view\":{\"id\":\"1075\"}},\"id\":\"1074\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1099\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1101\"},\"nonselection_glyph\":{\"id\":\"1100\"},\"view\":{\"id\":\"1103\"}},\"id\":\"1102\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1072\",\"type\":\"Circle\"}],\"root_ids\":[\"1005\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.3\"}};\n const render_items = [{\"docid\":\"14a00e77-c0d8-428a-ada5-8c30bc2ee6af\",\"root_ids\":[\"1005\"],\"roots\":{\"1005\":\"b266d0f6-784c-4b82-810c-b68dc9aba108\"}}];\n root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n }\n if (root.Bokeh !== undefined) {\n embed_document(root);\n } else {\n let attempts = 0;\n const timer = setInterval(function(root) {\n if (root.Bokeh !== undefined) {\n clearInterval(timer);\n embed_document(root);\n } else {\n attempts++;\n if (attempts > 100) {\n clearInterval(timer);\n console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n }\n }\n }, 10, root)\n }\n})(window);", + "application/javascript": "(function(root) {\n function embed_document(root) {\n const docs_json = {\"38256105-4b1d-4911-ab8c-3ce49cec427b\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1016\"}],\"center\":[{\"id\":\"1019\"},{\"id\":\"1023\"},{\"id\":\"1068\"}],\"height\":450,\"left\":[{\"id\":\"1020\"}],\"renderers\":[{\"id\":\"1044\"},{\"id\":\"1074\"},{\"id\":\"1102\"},{\"id\":\"1131\"},{\"id\":\"1159\"},{\"id\":\"1188\"},{\"id\":\"1216\"},{\"id\":\"1245\"}],\"title\":{\"id\":\"1006\"},\"toolbar\":{\"id\":\"1032\"},\"x_range\":{\"id\":\"1008\"},\"x_scale\":{\"id\":\"1012\"},\"y_range\":{\"id\":\"1010\"},\"y_scale\":{\"id\":\"1014\"}},\"id\":\"1005\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1132\",\"type\":\"CDSView\"},{\"attributes\":{\"fill_color\":{\"value\":\"green\"},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1128\",\"type\":\"Circle\"},{\"attributes\":{\"label\":{\"value\":\"cc\"},\"renderers\":[{\"id\":\"1216\"},{\"id\":\"1245\"}]},\"id\":\"1240\",\"type\":\"LegendItem\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1075\",\"type\":\"CDSView\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1215\",\"type\":\"Line\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1073\",\"type\":\"Circle\"},{\"attributes\":{\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1099\",\"type\":\"Line\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1071\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1073\"},\"nonselection_glyph\":{\"id\":\"1072\"},\"view\":{\"id\":\"1075\"}},\"id\":\"1074\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"cyan\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1243\",\"type\":\"Circle\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1099\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1101\"},\"nonselection_glyph\":{\"id\":\"1100\"},\"view\":{\"id\":\"1103\"}},\"id\":\"1102\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1041\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1043\"},\"nonselection_glyph\":{\"id\":\"1042\"},\"view\":{\"id\":\"1045\"}},\"id\":\"1044\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1103\",\"type\":\"CDSView\"},{\"attributes\":{\"days\":[1,4,7,10,13,16,19,22,25,28]},\"id\":\"1060\",\"type\":\"DaysTicker\"},{\"attributes\":{\"label\":{\"value\":\"jensen\"},\"renderers\":[{\"id\":\"1044\"},{\"id\":\"1074\"}]},\"id\":\"1069\",\"type\":\"LegendItem\"},{\"attributes\":{\"coordinates\":null,\"group\":null,\"text\":\"5x5 Wind Farm Timing Test\"},\"id\":\"1006\",\"type\":\"Title\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1030\",\"type\":\"BoxAnnotation\"},{\"attributes\":{\"fill_color\":{\"value\":\"cyan\"},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1242\",\"type\":\"Circle\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"green\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1130\",\"type\":\"Circle\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1072\",\"type\":\"Circle\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1043\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1042\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1010\",\"type\":\"DataRange1d\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1213\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1215\"},\"nonselection_glyph\":{\"id\":\"1214\"},\"view\":{\"id\":\"1217\"}},\"id\":\"1216\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1157\",\"type\":\"Line\"},{\"attributes\":{\"fill_color\":{\"value\":\"red\"},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1185\",\"type\":\"Circle\"},{\"attributes\":{\"data\":{\"cc\":{\"__ndarray__\":\"AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H+5jQbwFkj5P+f7qfHSTfk/6Ugu/yH9+D8c6+I2GsD5P5f/kH77Ovk/pN++Dpwz+T9t5/up8dL5P3lYqDXNO/k/u7iNBvAW+T+I9NvXgXP5Pz81XrpJDPo/rK3YX3ZP+T/XNO84RUf5P0aU9gZfmPk/AU2EDU+v+T+8BRIUP8b5P76fGi/dJPk/RiV1ApoI+j+Sy39Iv335PwXFjzF3Lfo/MuauJeSD+T/D0ytlGeL5P9uK/WX35Pk/rfpcbcX++T+OdXEbDeD5P/+ye/KwUPk/O3DOiNLe+T9DrWnecYr5PwIrhxbZzvk/CyQofoy5+T9uowG8BRL6P7AD54wo7fk/tTf4wmSq+T81XrpJDAL6P0tZhjjWxfk/U5YhjnVx+T+PwvUoXI/5PxZqTfOOU/k/guLHmLuW+T/EQq1p3nH5Pw4tsp3vp/k/ylTBqKRO+T8VHcnlP6T5P6pgVFInoPk/L90kBoGV+T9JLv8h/fb4PznWxW00gPk/bjSAt0CC+T+ppE5AE2H5P9obfGEyVfk/OdbFbTSA+T+XkA96Nqv0Pw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"code_coverage\":{\"__ndarray__\":\"PnlYqDXN2z/WVuwvuyfTPxB6Nqs+V9M/vJaQD3o21z8EVg4tsp3XP+PHmLuWkNc/lIeFWtO81z+Uh4Va07zXP9DVVuwvu9c/cvkP6bev1z/9h/Tb14HXP3sUrkfhetg/t2J/2T152D+QMXctIR/EP5Axdy0hH8Q/K/aX3ZOHxT+jkjoBTYTFP0T67evAOcM/ylTBqKROyD/8qfHSTWLIP662Yn/ZPck/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACutmJ/2T3JPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"commit_hash\":[\"df25a9cfacd3d652361d2bd37f568af00acb2631\",\"b797390a43298a815f3ff57955cfdc71ecf3e866\",\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\",\"dd847210082035d43b0273ae63a76a53cb8d2e12\",\"33779269e98cc882a5f066c462d8ec1eadf37a1a\",\"12890e029a7155b074b9b325d320d1798338e287\",\"66dafc08bd620d96deda7d526b0e4bfc3b086650\",\"a325819b3b03b84bd76ad455e3f9b4600744ba14\",\"8a2c1a610295c007f0222ce737723c341189811d\",\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\",\"03e1f461c152e4f221fe92c834f2787680cf5772\",\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\",\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\",\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\",\"d18f4d263ecabf502242592f9d60815a07c7b89c\",\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\",\"c2006b0011a5df036c306c15e75763ec492dafda\",\"0c2adf3e702b6427da946a6ba9dbedbea22738be\",\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\",\"8436fd78b002e5792f5d0dd1409332d171036d49\",\"07a45b66c5facfea06c40bd82e34040c97560640\",\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\",\"4d528a3d6456621a382d409b5145a877b5414b88\",\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\",\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\",\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\",\"26f06d449da208ce64724b1463b07ad20746cbdc\",\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\",\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\",\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\",\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\",\"16628a0ba45a675df762245694e0a7666a3478f8\",\"01684c8559604344bd09791268131819a09770a8\",\"e9231fb893c765b723fa4c1e087a58761b6aa471\",\"219889e243ffc69c71b6f7747f5af751d5694de1\",\"6124d2a82a7a823722210bc2e8516d355ba19eb3\",\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\",\"f2797fef396f2f19b02abb1f9555b678dac614f1\",\"b4e538f530048fec58eaca5170be82c67dbdcceb\",\"68820b715ed6b2c981aa11d29c0102e879280d79\",\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\",\"0d2bfecc271d561f67050659684b4797af8ee740\",\"1d03a465593f56c99a64a576d185d4ed17b659f2\",\"78a953b7ef9a36b62e5b446c80ed68abfddbfb74\",\"6c4f70ffbf3d4d2922d41d0032ae1b93d8a23c99\",\"ab03282623d0262b20b8c132efcdcace2dace766\",\"d2f7a45af27a6b40027d6f6a0f4f0be0c6dee5d9\",\"98b23f3d517481b127f190f5f8b7ebfae7f8b6b2\",\"452425de723cc1640d999022389672caf9bffbd0\",\"85dadb1a566c9fa8dc84cb9837b98bd5d23b8d58\",\"432ee7f96c1f6cccd05a0034c86c720cdb63a3e6\",\"ebd70ecaef14c0e239337eb6e36506303378a31a\",\"77fa7155d55bdf3fd43e29f58fe57feffcb107cf\",\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\",\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\",\"7c879f1ce18b52d9b0a8eecf877d03e66afc975b\",\"2aa9f2a55686f2ee5dc407e8e0223eb25176d906\",\"5e5bb7f4e653621e7a81ff4bcaa27dbc1f759de7\",\"d91953a499dfb88b457a1e7a07903debbda4058b\",\"76742879c81c9baced49b9fc60abbf1d2eba65ff\",\"9c73a41eaca95bb718ac79980a1799dfa1c48cf3\",\"67104dd714de939be136646af68edd9643ddfcd3\"],\"commit_hash_8char\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"03e1f461\",\"9e96d6c4\",\"2a98428f\",\"9b4e85cf\",\"d18f4d26\",\"a23241bb\",\"c2006b00\",\"0c2adf3e\",\"39c46600\",\"8436fd78\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"16628a0b\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\",\"78a953b7\",\"6c4f70ff\",\"ab032826\",\"d2f7a45a\",\"98b23f3d\",\"452425de\",\"85dadb1a\",\"432ee7f9\",\"ebd70eca\",\"77fa7155\",\"d5d4b134\",\"d5d4b134\",\"7c879f1c\",\"2aa9f2a5\",\"5e5bb7f4\",\"d91953a4\",\"76742879\",\"9c73a41e\",\"67104dd7\"],\"date\":{\"__ndarray__\":\"AACAyDfgd0IAAEDF0+F3QgAAACsm4ndCAACA9srid0IAAICNFOR3QgAAQPNm5HdCAAAAWbnkd0IAAMC+C+V3QgAAgCRe5XdCAACAJF7ld0IAAIC7p+Z3QgAAQCH65ndCAACA3Krvd0IAAABtLPR3QgAAQJ4j9XdCAAAAvML/d0IAAMBWixh4QgAAQIU5NHhCAAAAWyVJeEIAAAAn5WJ4QgAAACflYnhCAACAt2ZneEIAAEAduWd4QgAAQLQCaXhCAABAtAJpeEIAAMB/p2l4QgAAgHxDa3hCAACAfENreEIAAIB8Q2t4QgAAQOKVa3hCAABA4pVreEIAAEDilWt4QgAAwNvNbnhCAAAADcVveEIAAEA+vHB4QgAAAGnrdHhCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAMDvQH54QgAAwO9AfnhCAABAu+V+eEIAAEC75X54QgAAQLvlfnhCAABAu+V+eEIAAAAhOH94QgAAQFIvgHhCAABAUi+AeEIAAAC4gYB4QgAAwB3UgHhCAADAtB2CeEIAAMC0HYJ4QgAAwLQdgnhCAADAtB2CeEIAAMAQRId4QgAAwMiQkXhCAAAA+oeSeEIAAAD6h5J4Qg==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"gauss\":{\"__ndarray__\":\"GJXUCWgi9D+Hp1fKMsTzP0Jg5dAi2+k/xLEubqMB6j/LEMe6uI3qP9CzWfW52uo/1zTvOEVH6j/+1HjpJjHqPzhnRGlv8Ok/FR3J5T+k6T/MXUvIBz3pP99PjZduEuk/kst/SL996T/129eBc0bsPwmKH2PuWuw/P8bctYR87D+IY13cRgPsPw1xrIvbaOw/0m9fB84Z7T+94xQdyeXrP7snDwu1pus/r5RliGNd7D9sCfmgZ7PsP8UgsHJoke0/lWWIY13c7D8dWmQ730/tP2iz6nO1Fe0/46WbxCCw7D8AkX77OnDsP4Za07zjFO0/3+ALk6mC7T9txf6ye/LsP8X+snvysOw/XrpJDAIr7T+yne+nxkvtP+M2GsBbIO0/9+RhodY07T+h1jTvOEXtPzhnRGlv8O0/QKTfvg6c7T+qYFRSJ6DtP/mgZ7Pqc+0/6bevA+eM7D9R2ht8YTLtPwpoImx4eu0/1sVtNIC37D9oImx4eqXsP5MYBFYOLew/LSEf9GxW7T9n1edqK/brPz2bVZ+rrew/RUdy+Q/p6z9hVFInoInsP+m3rwPnjOw/KqkT0ETY7D9ZF7fRAN7sP9qs+lxtxew/N4lBYOXQ7D/D9Shcj8LrP6abxCCwcuw/kDF3LSEf7D+si9toAG/rPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"gch\":{\"__ndarray__\":\"X5hMFYxK+j9IUPwYc9f8P1UwKqkT0PU/BoGVQ4ts9T+gibDh6ZX2P+M2GsBbIPY/FYxK6gQ09j8OvjCZKhj2P9cS8kHPZvY/5WGh1jTv9j/LEMe6uI32P2q8dJMYBPY/f/s6cM6I9T8MAiuHFtn2P807TtGRXPc/ofgx5q4l9z+NKO0NvjD3P8rDQq1p3vY/bHh6pSxD9z/dJAaBlUP3P8dLN4lBYPc/63O1FfvL9j9UdCSX/5D2PzvfT42Xbvc/tFn1udqK9z/sL7snDwv4PyNKe4MvTPc/1QloImx49z+Hp1fKMsT2P4QNT6+UZfc/bjSAt0CC9z90JJf/kH73PxBYObTIdvc/CKwcWmQ79z+Cc0aU9gb3P1D8GHPXEvc/7FG4HoXr9z/l8h/Sb1/3P9jw9EpZhvc/MuauJeSD9z8JG55eKcv3P4BIv30dOPc/oyO5/If09j8xCKwcWmT3P2dEaW/whfc/JzEIrBxa9j/0bFZ9rrb2P73jFB3J5fY/ArwFEhQ/9z8yVTAqqRP3P7Pqc7UV+/Y/d76fGi/d9j/l0CLb+X72P2lv8IXJVPc/GeJYF7fR9j+Nl24Sg8D2P5kqGJXUCfc/bAn5oGez9j8J+aBns+r2P9V46SYxCPc/UiegibDh9j8nwoanV8rwPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61],\"jensen\":{\"__ndarray__\":\"WKg1zTtO9D/vOEVHcvnlP2EyVTAqqdc/fdCzWfW51j8f9GxWfa7WP9UJaCJseNY/r5RliGNd2D8awFsgQfHXP0Ck374OnNc/5fIf0m9f1z988rBQa5rXP6+UZYhjXdg/eVioNc07zj/Q1VbsL7vPP4y5awn5oM8/XI/C9Shczz9d3EYDeAvQP2WqYFRSJ9A/GJXUCWgi0D+coiO5/IfQP4qO5PIf0s8/f2q8dJMY0D+vlGWIY13QPzqSy39Iv9E/5q4l5IOe0T/zH9JvXwfSP+viNhrAW9A/QmDl0CLb0T8yVTAqqRPQP1InoImw4dE/ZRniWBe30T+2hHzQs1nRP5huEoPAytE/3bWEfNCz0T99rrZif9nRP451cRsN4NE/WvW52or90T8E54wo7Q3SP3uDL0ymCtI/CD2bVZ+r0T8cfGEyVTDSPw8LtaZ5x9E/pgpGJXUC0j+TGARWDi3SPzY8vVKWIdI/HThnRGlv0D8/xty1hHzQP8/3U+Olm9A/tTf4wmSq0D8BTYQNT6/QP8/3U+Olm9A/Ns07TtGR0D9oImx4eqXQP2/whclUwdA/aCJseHql0D/r4jYawFvQP9Ei2/l+atA/fT81XrpJ0D8u/yH99nXQP3RGlPYGX9A/xf6ye/Kw0D8Zc9cS8kHTPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"tooltip_label\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"PR #56\",\"v3.0rc1\",\"PR #317\",\"v3.0\",\"v3.0.1\",\"v3.1\",\"v3.1.1\",\"v3.2\",\"v3.2.1\",\"v3.2.2\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"v3.3\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\",\"78a953b7\",\"6c4f70ff\",\"ab032826\",\"d2f7a45a\",\"98b23f3d\",\"452425de\",\"85dadb1a\",\"432ee7f9\",\"ebd70eca\",\"77fa7155\",\"d5d4b134\",\"d5d4b134\",\"7c879f1c\",\"2aa9f2a5\",\"v3.4\",\"d91953a4\",\"76742879\",\"9c73a41e\",\"67104dd7\"]},\"selected\":{\"id\":\"1054\"},\"selection_policy\":{\"id\":\"1053\"}},\"id\":\"1003\",\"type\":\"ColumnDataSource\"},{\"attributes\":{},\"id\":\"1054\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"1053\",\"type\":\"UnionRenderers\"},{\"attributes\":{\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1041\",\"type\":\"Line\"},{\"attributes\":{\"fill_color\":{\"value\":\"blue\"},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"jensen\"}},\"id\":\"1071\",\"type\":\"Circle\"},{\"attributes\":{\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1156\",\"type\":\"Line\"},{\"attributes\":{\"days\":[1,8,15,22]},\"id\":\"1061\",\"type\":\"DaysTicker\"},{\"attributes\":{\"months\":[0,4,8]},\"id\":\"1065\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"days\":[1,15]},\"id\":\"1062\",\"type\":\"DaysTicker\"},{\"attributes\":{},\"id\":\"1008\",\"type\":\"DataRange1d\"},{\"attributes\":{\"base\":24,\"mantissas\":[1,2,4,6,8,12],\"max_interval\":43200000.0,\"min_interval\":3600000.0,\"num_minor_ticks\":0},\"id\":\"1058\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{},\"id\":\"1014\",\"type\":\"LinearScale\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"cyan\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"cyan\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1244\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1012\",\"type\":\"LinearScale\"},{\"attributes\":{\"months\":[0,1,2,3,4,5,6,7,8,9,10,11]},\"id\":\"1063\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1128\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1130\"},\"nonselection_glyph\":{\"id\":\"1129\"},\"view\":{\"id\":\"1132\"}},\"id\":\"1131\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"axis_label\":\"Commit date\",\"coordinates\":null,\"formatter\":{\"id\":\"1051\"},\"group\":null,\"major_label_policy\":{\"id\":\"1052\"},\"ticker\":{\"id\":\"1017\"}},\"id\":\"1016\",\"type\":\"DatetimeAxis\"},{\"attributes\":{\"num_minor_ticks\":5,\"tickers\":[{\"id\":\"1056\"},{\"id\":\"1057\"},{\"id\":\"1058\"},{\"id\":\"1059\"},{\"id\":\"1060\"},{\"id\":\"1061\"},{\"id\":\"1062\"},{\"id\":\"1063\"},{\"id\":\"1064\"},{\"id\":\"1065\"},{\"id\":\"1066\"},{\"id\":\"1067\"}]},\"id\":\"1017\",\"type\":\"DatetimeTicker\"},{\"attributes\":{\"days\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]},\"id\":\"1059\",\"type\":\"DaysTicker\"},{\"attributes\":{\"axis\":{\"id\":\"1016\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1019\",\"type\":\"Grid\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1156\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1158\"},\"nonselection_glyph\":{\"id\":\"1157\"},\"view\":{\"id\":\"1160\"}},\"id\":\"1159\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1160\",\"type\":\"CDSView\"},{\"attributes\":{\"base\":60,\"mantissas\":[1,2,5,10,15,20,30],\"max_interval\":1800000.0,\"min_interval\":1000.0,\"num_minor_ticks\":0},\"id\":\"1057\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"mantissas\":[1,2,5],\"max_interval\":500.0,\"num_minor_ticks\":0},\"id\":\"1056\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{},\"id\":\"1049\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1067\",\"type\":\"YearsTicker\"},{\"attributes\":{\"months\":[0,6]},\"id\":\"1066\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1045\",\"type\":\"CDSView\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1101\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"green\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1100\",\"type\":\"Line\"},{\"attributes\":{\"label\":{\"value\":\"gauss\"},\"renderers\":[{\"id\":\"1102\"},{\"id\":\"1131\"}]},\"id\":\"1126\",\"type\":\"LegendItem\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"green\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"green\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gauss\"}},\"id\":\"1129\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1051\",\"type\":\"DatetimeTickFormatter\"},{\"attributes\":{},\"id\":\"1052\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1048\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"axis_label\":\"Time to solution (s)\",\"coordinates\":null,\"formatter\":{\"id\":\"1048\"},\"group\":null,\"major_label_policy\":{\"id\":\"1049\"},\"ticker\":{\"id\":\"1021\"}},\"id\":\"1020\",\"type\":\"LinearAxis\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1246\",\"type\":\"CDSView\"},{\"attributes\":{\"axis\":{\"id\":\"1020\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1023\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1021\",\"type\":\"BasicTicker\"},{\"attributes\":{},\"id\":\"1028\",\"type\":\"ResetTool\"},{\"attributes\":{\"callback\":null,\"tooltips\":[[\"git ref\",\"@tooltip_label\"]]},\"id\":\"1031\",\"type\":\"HoverTool\"},{\"attributes\":{},\"id\":\"1029\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1025\",\"type\":\"WheelZoomTool\"},{\"attributes\":{},\"id\":\"1024\",\"type\":\"PanTool\"},{\"attributes\":{\"overlay\":{\"id\":\"1030\"}},\"id\":\"1026\",\"type\":\"BoxZoomTool\"},{\"attributes\":{},\"id\":\"1027\",\"type\":\"SaveTool\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"red\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1186\",\"type\":\"Circle\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1185\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1187\"},\"nonselection_glyph\":{\"id\":\"1186\"},\"view\":{\"id\":\"1189\"}},\"id\":\"1188\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1242\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1244\"},\"nonselection_glyph\":{\"id\":\"1243\"},\"view\":{\"id\":\"1246\"}},\"id\":\"1245\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1213\",\"type\":\"Line\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1189\",\"type\":\"CDSView\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"red\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"red\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1187\",\"type\":\"Circle\"},{\"attributes\":{\"months\":[0,2,4,6,8,10]},\"id\":\"1064\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"tools\":[{\"id\":\"1024\"},{\"id\":\"1025\"},{\"id\":\"1026\"},{\"id\":\"1027\"},{\"id\":\"1028\"},{\"id\":\"1029\"},{\"id\":\"1031\"}]},\"id\":\"1032\",\"type\":\"Toolbar\"},{\"attributes\":{\"label\":{\"value\":\"gch\"},\"renderers\":[{\"id\":\"1159\"},{\"id\":\"1188\"}]},\"id\":\"1183\",\"type\":\"LegendItem\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"red\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"gch\"}},\"id\":\"1158\",\"type\":\"Line\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1217\",\"type\":\"CDSView\"},{\"attributes\":{\"border_line_color\":\"black\",\"click_policy\":\"mute\",\"coordinates\":null,\"group\":null,\"items\":[{\"id\":\"1069\"},{\"id\":\"1126\"},{\"id\":\"1183\"},{\"id\":\"1240\"}],\"location\":\"bottom_left\"},\"id\":\"1068\",\"type\":\"Legend\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"cyan\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"cc\"}},\"id\":\"1214\",\"type\":\"Line\"}],\"root_ids\":[\"1005\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.3\"}};\n const render_items = [{\"docid\":\"38256105-4b1d-4911-ab8c-3ce49cec427b\",\"root_ids\":[\"1005\"],\"roots\":{\"1005\":\"730e09d0-024d-4350-a23d-4cf3dee3d66c\"}}];\n root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n }\n if (root.Bokeh !== undefined) {\n embed_document(root);\n } else {\n let attempts = 0;\n const timer = setInterval(function(root) {\n if (root.Bokeh !== undefined) {\n clearInterval(timer);\n embed_document(root);\n } else {\n attempts++;\n if (attempts > 100) {\n clearInterval(timer);\n console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n }\n }\n }, 10, root)\n }\n})(window);", "application/vnd.bokehjs_exec.v0+json": "" }, "metadata": { @@ -197,7 +217,7 @@ "data": { "text/html": [ "\n", - "
\n" + "
\n" ] }, "metadata": {}, @@ -205,7 +225,7 @@ }, { "data": { - "application/javascript": "(function(root) {\n function embed_document(root) {\n const docs_json = {\"77c55f8b-56eb-4d43-b824-70ae6cecde50\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1412\"}],\"center\":[{\"id\":\"1415\"},{\"id\":\"1419\"}],\"height\":450,\"left\":[{\"id\":\"1416\"}],\"renderers\":[{\"id\":\"1440\"},{\"id\":\"1446\"}],\"title\":{\"id\":\"1402\"},\"toolbar\":{\"id\":\"1428\"},\"x_range\":{\"id\":\"1404\"},\"x_scale\":{\"id\":\"1408\"},\"y_range\":{\"id\":\"1448\"},\"y_scale\":{\"id\":\"1410\"}},\"id\":\"1401\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{},\"id\":\"1492\",\"type\":\"YearsTicker\"},{\"attributes\":{\"data\":{\"cc\":{\"__ndarray__\":\"AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H9JLv8h/fb9P2gibHh6pf0/ZmZmZmZm/T+q8dJNYhD+P/7UeOkmMf4//yH99nXg/T/cRgN4CyT+P8BbIEHxY/0/tTf4wmSq/T8ydy0hH/T9P0oMAiuHFv4/INJvXwfO/T9q3nGKjuT9Px+F61G4Hv4/Di2yne+n/T+jI7n8h/T9P0Ck374OnP0/JXUCmggb/j/gnBGlvcH9PyegibDh6f0/0m9fB84Z/j9SuB6F61H+P7x0kxgEVv4/MlUwKqkT/j/l8h/Sb1/+PwMJih9j7v4/9bnaiv1l/j+DL0ymCkb+P2Rd3EYDeP4/3+ALk6mC/j8GgZVDi2z+P9NNYhBYOf4/uK8D54wo/T8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"code_coverage\":{\"__ndarray__\":\"PnlYqDXN2z/WVuwvuyfTPxB6Nqs+V9M/vJaQD3o21z8EVg4tsp3XP+PHmLuWkNc/lIeFWtO81z+Uh4Va07zXP9DVVuwvu9c/cvkP6bev1z/9h/Tb14HXP3sUrkfhetg/t2J/2T152D+QMXctIR/EP5Axdy0hH8Q/K/aX3ZOHxT+jkjoBTYTFP0T67evAOcM/ylTBqKROyD/8qfHSTWLIP662Yn/ZPck/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACutmJ/2T3JPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"commit_hash\":[\"df25a9cfacd3d652361d2bd37f568af00acb2631\",\"b797390a43298a815f3ff57955cfdc71ecf3e866\",\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\",\"dd847210082035d43b0273ae63a76a53cb8d2e12\",\"33779269e98cc882a5f066c462d8ec1eadf37a1a\",\"12890e029a7155b074b9b325d320d1798338e287\",\"66dafc08bd620d96deda7d526b0e4bfc3b086650\",\"a325819b3b03b84bd76ad455e3f9b4600744ba14\",\"8a2c1a610295c007f0222ce737723c341189811d\",\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\",\"03e1f461c152e4f221fe92c834f2787680cf5772\",\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\",\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\",\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\",\"d18f4d263ecabf502242592f9d60815a07c7b89c\",\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\",\"c2006b0011a5df036c306c15e75763ec492dafda\",\"0c2adf3e702b6427da946a6ba9dbedbea22738be\",\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\",\"8436fd78b002e5792f5d0dd1409332d171036d49\",\"07a45b66c5facfea06c40bd82e34040c97560640\",\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\",\"4d528a3d6456621a382d409b5145a877b5414b88\",\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\",\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\",\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\",\"26f06d449da208ce64724b1463b07ad20746cbdc\",\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\",\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\",\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\",\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\",\"16628a0ba45a675df762245694e0a7666a3478f8\",\"01684c8559604344bd09791268131819a09770a8\",\"e9231fb893c765b723fa4c1e087a58761b6aa471\",\"219889e243ffc69c71b6f7747f5af751d5694de1\",\"6124d2a82a7a823722210bc2e8516d355ba19eb3\",\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\",\"f2797fef396f2f19b02abb1f9555b678dac614f1\",\"b4e538f530048fec58eaca5170be82c67dbdcceb\",\"68820b715ed6b2c981aa11d29c0102e879280d79\",\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\",\"0d2bfecc271d561f67050659684b4797af8ee740\",\"1d03a465593f56c99a64a576d185d4ed17b659f2\"],\"commit_hash_8char\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"03e1f461\",\"9e96d6c4\",\"2a98428f\",\"9b4e85cf\",\"d18f4d26\",\"a23241bb\",\"c2006b00\",\"0c2adf3e\",\"39c46600\",\"8436fd78\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"16628a0b\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\"],\"date\":{\"__ndarray__\":\"AACAyDfgd0IAAEDF0+F3QgAAACsm4ndCAACA9srid0IAAICNFOR3QgAAQPNm5HdCAAAAWbnkd0IAAMC+C+V3QgAAgCRe5XdCAACAJF7ld0IAAIC7p+Z3QgAAQCH65ndCAACA3Krvd0IAAABtLPR3QgAAQJ4j9XdCAAAAvML/d0IAAMBWixh4QgAAQIU5NHhCAAAAWyVJeEIAAAAn5WJ4QgAAACflYnhCAACAt2ZneEIAAEAduWd4QgAAQLQCaXhCAABAtAJpeEIAAMB/p2l4QgAAgHxDa3hCAACAfENreEIAAIB8Q2t4QgAAQOKVa3hCAABA4pVreEIAAEDilWt4QgAAwNvNbnhCAAAADcVveEIAAEA+vHB4QgAAAGnrdHhCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEI=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"gauss\":{\"__ndarray__\":\"GJXUCWgi9D+Hp1fKMsTzP240gLdAguw/845TdCSX6z8s1JrmHafsPyZTBaOSOu0/MCqpE9BE7D9hw9MrZRnsP9jw9EpZhuw/pN++Dpwz7D/VeOkmMQjsP57vp8ZLN+s/pgpGJXUC7D/YgXNGlPbuP9lfdk8eFu4/LSEf9GxW7z/HuriNBvDuPzSitDf4wu4/RpT2Bl+Y7j/wFkhQ/BjvPwpoImx4eu8/bHh6pSxD7j8rGJXUCWjuPwn5oGez6u8/4JwRpb3B7z8HzhlR2hvwP6UsQxzr4u4/SOF6FK5H7z+oxks3iUHuP6Fns+pzte8/w/UoXI/C7z8K16NwPQrvPw8LtaZ5x+8/UiegibDh7z+IY13cRgPwP5SHhVrTvO8/GQRWDi2y7z92cRsN4C3wPxWMSuoENO8/escpOpLL7z/IBz2bVZ/vP/kP6bevA/A/gZVDi2zn7z8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"gch\":{\"__ndarray__\":\"X5hMFYxK+j9IUPwYc9f8Px3J5T+k3/c/2T15WKg19z9seHqlLEP4Pxzr4jYawPg/lPYGX5hM+D9xrIvbaAD4P4V80LNZ9fg/io7k8h/S9z/ChqdXyjL4P4XrUbgehfc/KxiV1Alo+D8730+Nl275P7x0kxgEVvk/seHplbIM+j988rBQa5r5P3EbDeAtkPk/ylTBqKRO+T/6fmq8dJP5PzMzMzMzM/k/HHxhMlUw+T8g0m9fB874P+84RUdy+fg/GCZTBaOS+T8Cmggbnl75PxE2PL1Slvk/3GgAb4EE+j80ETY8vVL5P4MvTKYKRvk/rIvbaABv+T9O0ZFc/kP5P2sr9pfdk/k/ylTBqKRO+T97gy9Mpgr5PzXvOEVHcvk/8WPMXUvI+T+e76fGSzf5P/hT46WbxPk/YTJVMCqp+T9XW7G/7J75P6+UZYhjXfk/6+I2GsBb+T8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],\"jensen\":{\"__ndarray__\":\"WKg1zTtO9D/vOEVHcvnlP2PuWkI+6Nk/qmBUUieg2T/D9Shcj8LZP4EExY8xd9k/uB6F61G42j8/V1uxv+zaP1+YTBWMSto/bAn5oGez2j/swDkjSnvbP7u4jQbwFtw/ylTBqKRO0D9R2ht8YTLRP3gLJCh+jNE/irDh6ZWy0D+Sy39Iv33RP/mgZ7Pqc9E/gQTFjzF30T/mriXkg57RP5Cg+DHmrtE/TYQNT6+U0T+GWtO84xTRP1tCPujZrNI/FNBE2PD00j+JQWDl0CLTPwyTqYJRSdE/nDOitDf40j8mUwWjkjrRP99PjZduEtM/tMh2vp8a0z8qOpLLf0jTP3ZPHhZqTdM/RiV1ApoI0z9DrWnecYrSP/kP6bevA9M/sHJoke180z+dgCbChqfTP/RsVn2uttI/KjqSy39I0z+Nl24Sg8DSP5+rrdhfdtM/p3nHKTqS0z8=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[43]},\"tooltip_label\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"PR #56\",\"v3.0rc1\",\"PR #317\",\"v3.0\",\"v3.0.1\",\"v3.1\",\"v3.1.1\",\"v3.2\",\"v3.2.1\",\"v3.2.2\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"v3.3\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\"]},\"selected\":{\"id\":\"1054\"},\"selection_policy\":{\"id\":\"1053\"}},\"id\":\"1003\",\"type\":\"ColumnDataSource\"},{\"attributes\":{},\"id\":\"1421\",\"type\":\"WheelZoomTool\"},{\"attributes\":{\"base\":60,\"mantissas\":[1,2,5,10,15,20,30],\"max_interval\":1800000.0,\"min_interval\":1000.0,\"num_minor_ticks\":0},\"id\":\"1482\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"coordinates\":null,\"group\":null,\"text\":\"Code Coverage\"},\"id\":\"1402\",\"type\":\"Title\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1445\",\"type\":\"Circle\"},{\"attributes\":{\"days\":[1,8,15,22]},\"id\":\"1486\",\"type\":\"DaysTicker\"},{\"attributes\":{\"callback\":null,\"tooltips\":[[\"git ref\",\"@tooltip_label\"]]},\"id\":\"1427\",\"type\":\"HoverTool\"},{\"attributes\":{\"tools\":[{\"id\":\"1420\"},{\"id\":\"1421\"},{\"id\":\"1422\"},{\"id\":\"1423\"},{\"id\":\"1424\"},{\"id\":\"1425\"},{\"id\":\"1427\"}]},\"id\":\"1428\",\"type\":\"Toolbar\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1443\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1445\"},\"nonselection_glyph\":{\"id\":\"1444\"},\"view\":{\"id\":\"1447\"}},\"id\":\"1446\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"overlay\":{\"id\":\"1426\"}},\"id\":\"1422\",\"type\":\"BoxZoomTool\"},{\"attributes\":{\"months\":[0,2,4,6,8,10]},\"id\":\"1489\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1426\",\"type\":\"BoxAnnotation\"},{\"attributes\":{\"axis_label\":\"Test coverage as a percentage of Python code\",\"coordinates\":null,\"formatter\":{\"id\":\"1473\"},\"group\":null,\"major_label_policy\":{\"id\":\"1474\"},\"ticker\":{\"id\":\"1417\"}},\"id\":\"1416\",\"type\":\"LinearAxis\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1441\",\"type\":\"CDSView\"},{\"attributes\":{\"axis\":{\"id\":\"1416\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1419\",\"type\":\"Grid\"},{\"attributes\":{\"months\":[0,6]},\"id\":\"1491\",\"type\":\"MonthsTicker\"},{\"attributes\":{},\"id\":\"1425\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1476\",\"type\":\"DatetimeTickFormatter\"},{\"attributes\":{\"days\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]},\"id\":\"1484\",\"type\":\"DaysTicker\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1438\",\"type\":\"Line\"},{\"attributes\":{\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1437\",\"type\":\"Line\"},{\"attributes\":{\"months\":[0,4,8]},\"id\":\"1490\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"months\":[0,1,2,3,4,5,6,7,8,9,10,11]},\"id\":\"1488\",\"type\":\"MonthsTicker\"},{\"attributes\":{},\"id\":\"1417\",\"type\":\"BasicTicker\"},{\"attributes\":{},\"id\":\"1448\",\"type\":\"Range1d\"},{\"attributes\":{\"mantissas\":[1,2,5],\"max_interval\":500.0,\"num_minor_ticks\":0},\"id\":\"1481\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1444\",\"type\":\"Circle\"},{\"attributes\":{\"base\":24,\"mantissas\":[1,2,4,6,8,12],\"max_interval\":43200000.0,\"min_interval\":3600000.0,\"num_minor_ticks\":0},\"id\":\"1483\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"days\":[1,4,7,10,13,16,19,22,25,28]},\"id\":\"1485\",\"type\":\"DaysTicker\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1447\",\"type\":\"CDSView\"},{\"attributes\":{\"days\":[1,15]},\"id\":\"1487\",\"type\":\"DaysTicker\"},{\"attributes\":{\"num_minor_ticks\":5,\"tickers\":[{\"id\":\"1481\"},{\"id\":\"1482\"},{\"id\":\"1483\"},{\"id\":\"1484\"},{\"id\":\"1485\"},{\"id\":\"1486\"},{\"id\":\"1487\"},{\"id\":\"1488\"},{\"id\":\"1489\"},{\"id\":\"1490\"},{\"id\":\"1491\"},{\"id\":\"1492\"}]},\"id\":\"1413\",\"type\":\"DatetimeTicker\"},{\"attributes\":{\"axis_label\":\"Commit date\",\"coordinates\":null,\"formatter\":{\"id\":\"1476\"},\"group\":null,\"major_label_policy\":{\"id\":\"1477\"},\"ticker\":{\"id\":\"1413\"}},\"id\":\"1412\",\"type\":\"DatetimeAxis\"},{\"attributes\":{},\"id\":\"1424\",\"type\":\"ResetTool\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1439\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1410\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"1420\",\"type\":\"PanTool\"},{\"attributes\":{},\"id\":\"1054\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"1404\",\"type\":\"DataRange1d\"},{\"attributes\":{},\"id\":\"1473\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"1053\",\"type\":\"UnionRenderers\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1437\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1439\"},\"nonselection_glyph\":{\"id\":\"1438\"},\"view\":{\"id\":\"1441\"}},\"id\":\"1440\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"1408\",\"type\":\"LinearScale\"},{\"attributes\":{\"fill_color\":{\"value\":\"blue\"},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1443\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1474\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1477\",\"type\":\"AllLabels\"},{\"attributes\":{\"axis\":{\"id\":\"1412\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1415\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1423\",\"type\":\"SaveTool\"}],\"root_ids\":[\"1401\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.3\"}};\n const render_items = [{\"docid\":\"77c55f8b-56eb-4d43-b824-70ae6cecde50\",\"root_ids\":[\"1401\"],\"roots\":{\"1401\":\"2899bfad-82e0-488a-8429-7b02da60aab4\"}}];\n root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n }\n if (root.Bokeh !== undefined) {\n embed_document(root);\n } else {\n let attempts = 0;\n const timer = setInterval(function(root) {\n if (root.Bokeh !== undefined) {\n clearInterval(timer);\n embed_document(root);\n } else {\n attempts++;\n if (attempts > 100) {\n clearInterval(timer);\n console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n }\n }\n }, 10, root)\n }\n})(window);", + "application/javascript": "(function(root) {\n function embed_document(root) {\n const docs_json = {\"bb3868ef-0752-402f-883a-2db367d65446\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1412\"}],\"center\":[{\"id\":\"1415\"},{\"id\":\"1419\"}],\"height\":450,\"left\":[{\"id\":\"1416\"}],\"renderers\":[{\"id\":\"1440\"},{\"id\":\"1446\"}],\"title\":{\"id\":\"1402\"},\"toolbar\":{\"id\":\"1428\"},\"x_range\":{\"id\":\"1404\"},\"x_scale\":{\"id\":\"1408\"},\"y_range\":{\"id\":\"1448\"},\"y_scale\":{\"id\":\"1410\"}},\"id\":\"1401\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{\"axis_label\":\"Commit date\",\"coordinates\":null,\"formatter\":{\"id\":\"1476\"},\"group\":null,\"major_label_policy\":{\"id\":\"1477\"},\"ticker\":{\"id\":\"1413\"}},\"id\":\"1412\",\"type\":\"DatetimeAxis\"},{\"attributes\":{\"coordinates\":null,\"group\":null,\"text\":\"Code Coverage\"},\"id\":\"1402\",\"type\":\"Title\"},{\"attributes\":{\"base\":24,\"mantissas\":[1,2,4,6,8,12],\"max_interval\":43200000.0,\"min_interval\":3600000.0,\"num_minor_ticks\":0},\"id\":\"1483\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"axis\":{\"id\":\"1412\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1415\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1410\",\"type\":\"LinearScale\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1443\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1445\"},\"nonselection_glyph\":{\"id\":\"1444\"},\"view\":{\"id\":\"1447\"}},\"id\":\"1446\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"months\":[0,1,2,3,4,5,6,7,8,9,10,11]},\"id\":\"1488\",\"type\":\"MonthsTicker\"},{\"attributes\":{},\"id\":\"1420\",\"type\":\"PanTool\"},{\"attributes\":{\"days\":[1,15]},\"id\":\"1487\",\"type\":\"DaysTicker\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1003\"},\"glyph\":{\"id\":\"1437\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1439\"},\"nonselection_glyph\":{\"id\":\"1438\"},\"view\":{\"id\":\"1441\"}},\"id\":\"1440\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"days\":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]},\"id\":\"1484\",\"type\":\"DaysTicker\"},{\"attributes\":{\"days\":[1,4,7,10,13,16,19,22,25,28]},\"id\":\"1485\",\"type\":\"DaysTicker\"},{\"attributes\":{\"days\":[1,8,15,22]},\"id\":\"1486\",\"type\":\"DaysTicker\"},{\"attributes\":{},\"id\":\"1421\",\"type\":\"WheelZoomTool\"},{\"attributes\":{\"months\":[0,6]},\"id\":\"1491\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"months\":[0,2,4,6,8,10]},\"id\":\"1489\",\"type\":\"MonthsTicker\"},{\"attributes\":{\"months\":[0,4,8]},\"id\":\"1490\",\"type\":\"MonthsTicker\"},{\"attributes\":{},\"id\":\"1473\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"1425\",\"type\":\"HelpTool\"},{\"attributes\":{\"fill_color\":{\"value\":\"blue\"},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1443\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1474\",\"type\":\"AllLabels\"},{\"attributes\":{\"axis\":{\"id\":\"1416\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1419\",\"type\":\"Grid\"},{\"attributes\":{\"axis_label\":\"Test coverage as a percentage of Python code\",\"coordinates\":null,\"formatter\":{\"id\":\"1473\"},\"group\":null,\"major_label_policy\":{\"id\":\"1474\"},\"ticker\":{\"id\":\"1417\"}},\"id\":\"1416\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"1417\",\"type\":\"BasicTicker\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.2},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1445\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1404\",\"type\":\"DataRange1d\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"blue\"},\"hatch_alpha\":{\"value\":0.1},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"blue\"},\"size\":{\"value\":6},\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1444\",\"type\":\"Circle\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1447\",\"type\":\"CDSView\"},{\"attributes\":{},\"id\":\"1477\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1423\",\"type\":\"SaveTool\"},{\"attributes\":{\"tools\":[{\"id\":\"1420\"},{\"id\":\"1421\"},{\"id\":\"1422\"},{\"id\":\"1423\"},{\"id\":\"1424\"},{\"id\":\"1425\"},{\"id\":\"1427\"}]},\"id\":\"1428\",\"type\":\"Toolbar\"},{\"attributes\":{\"overlay\":{\"id\":\"1426\"}},\"id\":\"1422\",\"type\":\"BoxZoomTool\"},{\"attributes\":{\"callback\":null,\"tooltips\":[[\"git ref\",\"@tooltip_label\"]]},\"id\":\"1427\",\"type\":\"HoverTool\"},{\"attributes\":{},\"id\":\"1424\",\"type\":\"ResetTool\"},{\"attributes\":{\"mantissas\":[1,2,5],\"max_interval\":500.0,\"num_minor_ticks\":0},\"id\":\"1481\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1437\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1439\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1448\",\"type\":\"Range1d\"},{\"attributes\":{},\"id\":\"1408\",\"type\":\"LinearScale\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1426\",\"type\":\"BoxAnnotation\"},{\"attributes\":{\"base\":60,\"mantissas\":[1,2,5,10,15,20,30],\"max_interval\":1800000.0,\"min_interval\":1000.0,\"num_minor_ticks\":0},\"id\":\"1482\",\"type\":\"AdaptiveTicker\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"blue\",\"x\":{\"field\":\"date\"},\"y\":{\"field\":\"code_coverage\"}},\"id\":\"1438\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1476\",\"type\":\"DatetimeTickFormatter\"},{\"attributes\":{\"source\":{\"id\":\"1003\"}},\"id\":\"1441\",\"type\":\"CDSView\"},{\"attributes\":{},\"id\":\"1492\",\"type\":\"YearsTicker\"},{\"attributes\":{\"data\":{\"cc\":{\"__ndarray__\":\"AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H+5jQbwFkj5P+f7qfHSTfk/6Ugu/yH9+D8c6+I2GsD5P5f/kH77Ovk/pN++Dpwz+T9t5/up8dL5P3lYqDXNO/k/u7iNBvAW+T+I9NvXgXP5Pz81XrpJDPo/rK3YX3ZP+T/XNO84RUf5P0aU9gZfmPk/AU2EDU+v+T+8BRIUP8b5P76fGi/dJPk/RiV1ApoI+j+Sy39Iv335PwXFjzF3Lfo/MuauJeSD+T/D0ytlGeL5P9uK/WX35Pk/rfpcbcX++T+OdXEbDeD5P/+ye/KwUPk/O3DOiNLe+T9DrWnecYr5PwIrhxbZzvk/CyQofoy5+T9uowG8BRL6P7AD54wo7fk/tTf4wmSq+T81XrpJDAL6P0tZhjjWxfk/U5YhjnVx+T+PwvUoXI/5PxZqTfOOU/k/guLHmLuW+T/EQq1p3nH5Pw4tsp3vp/k/ylTBqKRO+T8VHcnlP6T5P6pgVFInoPk/L90kBoGV+T9JLv8h/fb4PznWxW00gPk/bjSAt0CC+T+ppE5AE2H5P9obfGEyVfk/OdbFbTSA+T+XkA96Nqv0Pw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"code_coverage\":{\"__ndarray__\":\"PnlYqDXN2z/WVuwvuyfTPxB6Nqs+V9M/vJaQD3o21z8EVg4tsp3XP+PHmLuWkNc/lIeFWtO81z+Uh4Va07zXP9DVVuwvu9c/cvkP6bev1z/9h/Tb14HXP3sUrkfhetg/t2J/2T152D+QMXctIR/EP5Axdy0hH8Q/K/aX3ZOHxT+jkjoBTYTFP0T67evAOcM/ylTBqKROyD/8qfHSTWLIP662Yn/ZPck/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACutmJ/2T3JPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"commit_hash\":[\"df25a9cfacd3d652361d2bd37f568af00acb2631\",\"b797390a43298a815f3ff57955cfdc71ecf3e866\",\"01a02d5f91b2f4a863eebe88a618974b0749d1c4\",\"dd847210082035d43b0273ae63a76a53cb8d2e12\",\"33779269e98cc882a5f066c462d8ec1eadf37a1a\",\"12890e029a7155b074b9b325d320d1798338e287\",\"66dafc08bd620d96deda7d526b0e4bfc3b086650\",\"a325819b3b03b84bd76ad455e3f9b4600744ba14\",\"8a2c1a610295c007f0222ce737723c341189811d\",\"c6bc79b0cfbc8ce5d6da0d33b68028157d2e93c0\",\"03e1f461c152e4f221fe92c834f2787680cf5772\",\"9e96d6c412b64fe76a57e7de8af3b00c21d18348\",\"2a98428f9c6fb9bb4302ae09809441bf3e7162b0\",\"9b4e85cf1b41ba7001aaba1a830b93e176f3dd43\",\"d18f4d263ecabf502242592f9d60815a07c7b89c\",\"a23241bb9e45078e36a4662d48c9d3fe0c3316e4\",\"c2006b0011a5df036c306c15e75763ec492dafda\",\"0c2adf3e702b6427da946a6ba9dbedbea22738be\",\"39c466000b1874e06a6f58da9c30bb877fc8d4d2\",\"8436fd78b002e5792f5d0dd1409332d171036d49\",\"07a45b66c5facfea06c40bd82e34040c97560640\",\"1d84538c334a502c6ad7df48b8cc2309d6a6436d\",\"4d528a3d6456621a382d409b5145a877b5414b88\",\"8c637b36b66069b216cb94ae87d4c0a91e9b211e\",\"4d23fa6dd78d0497deb4fd62783f0b3ee4204579\",\"015f6874c320efee2c0d1ae76eea4a5b043d69d6\",\"26f06d449da208ce64724b1463b07ad20746cbdc\",\"6b9d6bb8bec6e3ea548f5858e2a8ea5986264fc8\",\"b796bd0fd92ba6b91d590f6cb60bb7ab3bca9932\",\"780aef7c7b4b9cafea3e323d536a34a4af5818b4\",\"9f93ad9bf85e4a0e6baf5b62ea4b3ef143729861\",\"16628a0ba45a675df762245694e0a7666a3478f8\",\"01684c8559604344bd09791268131819a09770a8\",\"e9231fb893c765b723fa4c1e087a58761b6aa471\",\"219889e243ffc69c71b6f7747f5af751d5694de1\",\"6124d2a82a7a823722210bc2e8516d355ba19eb3\",\"f6e4287f712cc866893e71b1ea7a7546e4567bf9\",\"f2797fef396f2f19b02abb1f9555b678dac614f1\",\"b4e538f530048fec58eaca5170be82c67dbdcceb\",\"68820b715ed6b2c981aa11d29c0102e879280d79\",\"03deffeda91fa8d8ab188d57b9fa302a7be008e0\",\"0d2bfecc271d561f67050659684b4797af8ee740\",\"1d03a465593f56c99a64a576d185d4ed17b659f2\",\"78a953b7ef9a36b62e5b446c80ed68abfddbfb74\",\"6c4f70ffbf3d4d2922d41d0032ae1b93d8a23c99\",\"ab03282623d0262b20b8c132efcdcace2dace766\",\"d2f7a45af27a6b40027d6f6a0f4f0be0c6dee5d9\",\"98b23f3d517481b127f190f5f8b7ebfae7f8b6b2\",\"452425de723cc1640d999022389672caf9bffbd0\",\"85dadb1a566c9fa8dc84cb9837b98bd5d23b8d58\",\"432ee7f96c1f6cccd05a0034c86c720cdb63a3e6\",\"ebd70ecaef14c0e239337eb6e36506303378a31a\",\"77fa7155d55bdf3fd43e29f58fe57feffcb107cf\",\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\",\"d5d4b1346bd6acba9ba41b4bf546640de162a9d6\",\"7c879f1ce18b52d9b0a8eecf877d03e66afc975b\",\"2aa9f2a55686f2ee5dc407e8e0223eb25176d906\",\"5e5bb7f4e653621e7a81ff4bcaa27dbc1f759de7\",\"d91953a499dfb88b457a1e7a07903debbda4058b\",\"76742879c81c9baced49b9fc60abbf1d2eba65ff\",\"9c73a41eaca95bb718ac79980a1799dfa1c48cf3\",\"67104dd714de939be136646af68edd9643ddfcd3\"],\"commit_hash_8char\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"03e1f461\",\"9e96d6c4\",\"2a98428f\",\"9b4e85cf\",\"d18f4d26\",\"a23241bb\",\"c2006b00\",\"0c2adf3e\",\"39c46600\",\"8436fd78\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"16628a0b\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\",\"78a953b7\",\"6c4f70ff\",\"ab032826\",\"d2f7a45a\",\"98b23f3d\",\"452425de\",\"85dadb1a\",\"432ee7f9\",\"ebd70eca\",\"77fa7155\",\"d5d4b134\",\"d5d4b134\",\"7c879f1c\",\"2aa9f2a5\",\"5e5bb7f4\",\"d91953a4\",\"76742879\",\"9c73a41e\",\"67104dd7\"],\"date\":{\"__ndarray__\":\"AACAyDfgd0IAAEDF0+F3QgAAACsm4ndCAACA9srid0IAAICNFOR3QgAAQPNm5HdCAAAAWbnkd0IAAMC+C+V3QgAAgCRe5XdCAACAJF7ld0IAAIC7p+Z3QgAAQCH65ndCAACA3Krvd0IAAABtLPR3QgAAQJ4j9XdCAAAAvML/d0IAAMBWixh4QgAAQIU5NHhCAAAAWyVJeEIAAAAn5WJ4QgAAACflYnhCAACAt2ZneEIAAEAduWd4QgAAQLQCaXhCAABAtAJpeEIAAMB/p2l4QgAAgHxDa3hCAACAfENreEIAAIB8Q2t4QgAAQOKVa3hCAABA4pVreEIAAEDilWt4QgAAwNvNbnhCAAAADcVveEIAAEA+vHB4QgAAAGnrdHhCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAABcW3t4QgAAAFxbe3hCAAAAXFt7eEIAAMDvQH54QgAAwO9AfnhCAABAu+V+eEIAAEC75X54QgAAQLvlfnhCAABAu+V+eEIAAAAhOH94QgAAQFIvgHhCAABAUi+AeEIAAAC4gYB4QgAAwB3UgHhCAADAtB2CeEIAAMC0HYJ4QgAAwLQdgnhCAADAtB2CeEIAAMAQRId4QgAAwMiQkXhCAAAA+oeSeEIAAAD6h5J4Qg==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"gauss\":{\"__ndarray__\":\"GJXUCWgi9D+Hp1fKMsTzP0Jg5dAi2+k/xLEubqMB6j/LEMe6uI3qP9CzWfW52uo/1zTvOEVH6j/+1HjpJjHqPzhnRGlv8Ok/FR3J5T+k6T/MXUvIBz3pP99PjZduEuk/kst/SL996T/129eBc0bsPwmKH2PuWuw/P8bctYR87D+IY13cRgPsPw1xrIvbaOw/0m9fB84Z7T+94xQdyeXrP7snDwu1pus/r5RliGNd7D9sCfmgZ7PsP8UgsHJoke0/lWWIY13c7D8dWmQ730/tP2iz6nO1Fe0/46WbxCCw7D8AkX77OnDsP4Za07zjFO0/3+ALk6mC7T9txf6ye/LsP8X+snvysOw/XrpJDAIr7T+yne+nxkvtP+M2GsBbIO0/9+RhodY07T+h1jTvOEXtPzhnRGlv8O0/QKTfvg6c7T+qYFRSJ6DtP/mgZ7Pqc+0/6bevA+eM7D9R2ht8YTLtPwpoImx4eu0/1sVtNIC37D9oImx4eqXsP5MYBFYOLew/LSEf9GxW7T9n1edqK/brPz2bVZ+rrew/RUdy+Q/p6z9hVFInoInsP+m3rwPnjOw/KqkT0ETY7D9ZF7fRAN7sP9qs+lxtxew/N4lBYOXQ7D/D9Shcj8LrP6abxCCwcuw/kDF3LSEf7D+si9toAG/rPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"gch\":{\"__ndarray__\":\"X5hMFYxK+j9IUPwYc9f8P1UwKqkT0PU/BoGVQ4ts9T+gibDh6ZX2P+M2GsBbIPY/FYxK6gQ09j8OvjCZKhj2P9cS8kHPZvY/5WGh1jTv9j/LEMe6uI32P2q8dJMYBPY/f/s6cM6I9T8MAiuHFtn2P807TtGRXPc/ofgx5q4l9z+NKO0NvjD3P8rDQq1p3vY/bHh6pSxD9z/dJAaBlUP3P8dLN4lBYPc/63O1FfvL9j9UdCSX/5D2PzvfT42Xbvc/tFn1udqK9z/sL7snDwv4PyNKe4MvTPc/1QloImx49z+Hp1fKMsT2P4QNT6+UZfc/bjSAt0CC9z90JJf/kH73PxBYObTIdvc/CKwcWmQ79z+Cc0aU9gb3P1D8GHPXEvc/7FG4HoXr9z/l8h/Sb1/3P9jw9EpZhvc/MuauJeSD9z8JG55eKcv3P4BIv30dOPc/oyO5/If09j8xCKwcWmT3P2dEaW/whfc/JzEIrBxa9j/0bFZ9rrb2P73jFB3J5fY/ArwFEhQ/9z8yVTAqqRP3P7Pqc7UV+/Y/d76fGi/d9j/l0CLb+X72P2lv8IXJVPc/GeJYF7fR9j+Nl24Sg8D2P5kqGJXUCfc/bAn5oGez9j8J+aBns+r2P9V46SYxCPc/UiegibDh9j8nwoanV8rwPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61],\"jensen\":{\"__ndarray__\":\"WKg1zTtO9D/vOEVHcvnlP2EyVTAqqdc/fdCzWfW51j8f9GxWfa7WP9UJaCJseNY/r5RliGNd2D8awFsgQfHXP0Ck374OnNc/5fIf0m9f1z988rBQa5rXP6+UZYhjXdg/eVioNc07zj/Q1VbsL7vPP4y5awn5oM8/XI/C9Shczz9d3EYDeAvQP2WqYFRSJ9A/GJXUCWgi0D+coiO5/IfQP4qO5PIf0s8/f2q8dJMY0D+vlGWIY13QPzqSy39Iv9E/5q4l5IOe0T/zH9JvXwfSP+viNhrAW9A/QmDl0CLb0T8yVTAqqRPQP1InoImw4dE/ZRniWBe30T+2hHzQs1nRP5huEoPAytE/3bWEfNCz0T99rrZif9nRP451cRsN4NE/WvW52or90T8E54wo7Q3SP3uDL0ymCtI/CD2bVZ+r0T8cfGEyVTDSPw8LtaZ5x9E/pgpGJXUC0j+TGARWDi3SPzY8vVKWIdI/HThnRGlv0D8/xty1hHzQP8/3U+Olm9A/tTf4wmSq0D8BTYQNT6/QP8/3U+Olm9A/Ns07TtGR0D9oImx4eqXQP2/whclUwdA/aCJseHql0D/r4jYawFvQP9Ei2/l+atA/fT81XrpJ0D8u/yH99nXQP3RGlPYGX9A/xf6ye/Kw0D8Zc9cS8kHTPw==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[62]},\"tooltip_label\":[\"df25a9cf\",\"b797390a\",\"01a02d5f\",\"dd847210\",\"33779269\",\"12890e02\",\"66dafc08\",\"a325819b\",\"8a2c1a61\",\"c6bc79b0\",\"PR #56\",\"v3.0rc1\",\"PR #317\",\"v3.0\",\"v3.0.1\",\"v3.1\",\"v3.1.1\",\"v3.2\",\"v3.2.1\",\"v3.2.2\",\"07a45b66\",\"1d84538c\",\"4d528a3d\",\"8c637b36\",\"4d23fa6d\",\"015f6874\",\"26f06d44\",\"6b9d6bb8\",\"b796bd0f\",\"780aef7c\",\"9f93ad9b\",\"v3.3\",\"01684c85\",\"e9231fb8\",\"219889e2\",\"6124d2a8\",\"f6e4287f\",\"f2797fef\",\"b4e538f5\",\"68820b71\",\"03deffed\",\"0d2bfecc\",\"1d03a465\",\"78a953b7\",\"6c4f70ff\",\"ab032826\",\"d2f7a45a\",\"98b23f3d\",\"452425de\",\"85dadb1a\",\"432ee7f9\",\"ebd70eca\",\"77fa7155\",\"d5d4b134\",\"d5d4b134\",\"7c879f1c\",\"2aa9f2a5\",\"v3.4\",\"d91953a4\",\"76742879\",\"9c73a41e\",\"67104dd7\"]},\"selected\":{\"id\":\"1054\"},\"selection_policy\":{\"id\":\"1053\"}},\"id\":\"1003\",\"type\":\"ColumnDataSource\"},{\"attributes\":{},\"id\":\"1054\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"1053\",\"type\":\"UnionRenderers\"},{\"attributes\":{\"num_minor_ticks\":5,\"tickers\":[{\"id\":\"1481\"},{\"id\":\"1482\"},{\"id\":\"1483\"},{\"id\":\"1484\"},{\"id\":\"1485\"},{\"id\":\"1486\"},{\"id\":\"1487\"},{\"id\":\"1488\"},{\"id\":\"1489\"},{\"id\":\"1490\"},{\"id\":\"1491\"},{\"id\":\"1492\"}]},\"id\":\"1413\",\"type\":\"DatetimeTicker\"}],\"root_ids\":[\"1401\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.3\"}};\n const render_items = [{\"docid\":\"bb3868ef-0752-402f-883a-2db367d65446\",\"root_ids\":[\"1401\"],\"roots\":{\"1401\":\"d87aac59-c6f2-4222-a00a-6786cf9e2391\"}}];\n root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n }\n if (root.Bokeh !== undefined) {\n embed_document(root);\n } else {\n let attempts = 0;\n const timer = setInterval(function(root) {\n if (root.Bokeh !== undefined) {\n clearInterval(timer);\n embed_document(root);\n } else {\n attempts++;\n if (attempts > 100) {\n clearInterval(timer);\n console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n }\n }\n }, 10, root)\n }\n})(window);", "application/vnd.bokehjs_exec.v0+json": "" }, "metadata": { diff --git a/docs/dev_guide.md b/docs/dev_guide.md index 290325531..ac1b32da2 100644 --- a/docs/dev_guide.md +++ b/docs/dev_guide.md @@ -216,7 +216,7 @@ compiling, a file should be located at ``docs/_build/html/index.html``. This file can be opened in any browser. ```bash -pip install -e .["docs"] +pip install -e ".[docs]" jupyter-book build docs/ # Lots of output to the terminal here... diff --git a/docs/empirical_gauss_model.md b/docs/empirical_gauss_model.md index 12078dcf2..c1c9fddf5 100644 --- a/docs/empirical_gauss_model.md +++ b/docs/empirical_gauss_model.md @@ -10,7 +10,9 @@ have been reorganized to provide simpler tuning and data fitting. The velocity deficit at a point $(x, y, z)$ in the wake follows a Gaussian curve, i.e., + $$ \frac{u}{U_\infty} = 1 - Ce^{-\frac{(y-\delta_y)^2}{2\sigma_y^2} -\frac{(z-z_h-\delta_z)^2}{2\sigma_z^2}} $$ + where the $(x, y, z)$ origin is at the turbine location (at ground level). The terms $C$, $\sigma_y$, $\sigma_z$, $\delta_y$, and $\delta_z$ all depend on the downstream location $x$. diff --git a/docs/examples.md b/docs/examples.md index cc38c4c2e..e87627cd4 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -84,8 +84,19 @@ Define non-uniform (heterogeneous) atmospheric conditions by specifying speedups at locations throughout the farm. Show plots of the impact on wind turbine wakes. -### 16b_heterogenaity_multiple_ws_wd.py -Illustrate usage of heterogenaity with multiple wind speeds and directions. +### 16b_heterogeneity_multiple_ws_wd.py +Illustrate usage of heterogeneity with multiple wind speeds and directions. + +## 16c_optimize_layout_with_heterogeneity.py +This example shows a layout optimization using the geometric yaw option. It +combines elements of examples 15 (layout optimization) and 16 (heterogeneous +inflow) for demonstrative purposes. If you haven't yet run those examples, +we recommend you try them first. + +Heterogeneity in the inflow provides the necessary driver for coupled yaw +and layout optimization to be worthwhile. First, a layout optimization is +run without coupled yaw optimization; then a coupled optimization is run to +show the benefits of coupled optimization when flows are heterogeneous. ### 17_multiple_turbine_types.py Load an input file that describes a wind farm with two turbines @@ -99,6 +110,10 @@ of a turbine layout within FLORIS. Demonstrates the definition of a floating turbine and how to enable the effects of tilt on Cp and Ct. +For further examples on floating wind turbines, see also examples +25 (vertical wake deflection by a forced tilt angle) and 29 (comparison between +a fixed-bottom and floating wind farm). + ### 25_tilt_driven_vertical_wake_deflection.py This example demonstrates vertical wake deflections due to the tilt angle when running @@ -107,6 +122,10 @@ vertical deflections at this time. Also be aware that this example uses a potent unrealistic tilt angle, 15 degrees, to highlight the wake deflection. Moreover, the magnitude of vertical deflections due to tilt has not been validated. +For further examples on floating wind turbines, see also examples +24 (effects of tilt on turbine power and thrust coefficients) and 29 +(comparison between a fixed-bottom and floating wind farm). + ### 26_empirical_gauss_velocity_deficit_parameters.py This example illustrates the main parameters of the Empirical Gaussian @@ -127,6 +146,32 @@ mast across all wind directions (at a fixed free stream wind speed). Try different values for met_mast_option to vary the location of the met mast within the two-turbine farm. +### 29_floating_vs_fixedbottom_farm.py + +Compares a fixed-bottom wind farm (with a gridded layout) to a floating +wind farm with the same layout. Includes: +- Turbine-by-turbine power comparison for a single wind speed and direction +- Flow visualizations for a single wind speed and direction +- AEP calculations based on an example wind rose. + +For further examples on floating wind turbines, see also examples +24 (effects of tilt on turbine power and thrust coefficients) and 25 +(vertical wake deflection by a forced tilt angle). + +### 30_multi_dimensional_cp_ct.py + +This example showcases the capability of using multi-dimensional Cp/Ct data in turbine defintions +dependent on external conditions. Specifically, fictional data for varying Cp/Ct values based on +wave period, Ts, and wave height, Hs, is used, showing the user how to setup the turbine +definition and input file. Also demonstrated is the different method for getting turbine +powers when using multi-dimensional Cp/Ct data. + +### 31_multi_dimensional_cp_ct_2Hs.py + +This example builds on example 30. Specifically, fictional data for varying Cp/Ct values based on +wave period, Ts, and wave height, Hs, is used to show the difference in power performance for +different wave heights. + ## Optimization These examples demonstrate use of the optimization routines diff --git a/examples/01_opening_floris_computing_power.py b/examples/01_opening_floris_computing_power.py index 049718f38..b006dfe4d 100644 --- a/examples/01_opening_floris_computing_power.py +++ b/examples/01_opening_floris_computing_power.py @@ -51,7 +51,7 @@ print(turbine_powers) print("Shape: ",turbine_powers.shape) -# Single wind speed and wind direction +# Single wind speed and multiple wind directions print('\n========================= Single Wind Direction and Multiple Wind Speeds ===============') @@ -64,7 +64,7 @@ print(turbine_powers) print("Shape: ",turbine_powers.shape) -# Single wind speed and wind direction +# Multiple wind speeds and multiple wind directions print('\n========================= Multiple Wind Directions and Multiple Wind Speeds ============') wind_directions = np.array([260., 270., 280.]) diff --git a/examples/02_visualizations.py b/examples/02_visualizations.py index 669e91fa0..4b65f8e9d 100644 --- a/examples/02_visualizations.py +++ b/examples/02_visualizations.py @@ -18,10 +18,6 @@ import floris.tools.visualization as wakeviz from floris.tools import FlorisInterface -from floris.tools.visualization import ( - calculate_horizontal_plane_with_turbines, - visualize_cut_plane, -) """ @@ -78,13 +74,28 @@ # Create the plots fig, ax_list = plt.subplots(3, 1, figsize=(10, 8)) ax_list = ax_list.flatten() -wakeviz.visualize_cut_plane(horizontal_plane, ax=ax_list[0], title="Horizontal") -wakeviz.visualize_cut_plane(y_plane, ax=ax_list[1], title="Streamwise profile") -wakeviz.visualize_cut_plane(cross_plane, ax=ax_list[2], title="Spanwise profile") +wakeviz.visualize_cut_plane( + horizontal_plane, + ax=ax_list[0], + label_contours=True, + title="Horizontal" +) +wakeviz.visualize_cut_plane( + y_plane, + ax=ax_list[1], + label_contours=True, + title="Streamwise profile" +) +wakeviz.visualize_cut_plane( + cross_plane, + ax=ax_list[2], + label_contours=True, + title="Spanwise profile" +) # Some wake models may not yet have a visualization method included, for these cases can use # a slower version which scans a turbine model to produce the horizontal flow -horizontal_plane_scan_turbine = calculate_horizontal_plane_with_turbines( +horizontal_plane_scan_turbine = wakeviz.calculate_horizontal_plane_with_turbines( fi, x_resolution=20, y_resolution=10, @@ -92,9 +103,10 @@ ) fig, ax = plt.subplots() -visualize_cut_plane( +wakeviz.visualize_cut_plane( horizontal_plane_scan_turbine, ax=ax, + label_contours=True, title="Horizontal (coarse turbine scan method)", ) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index 5575779d1..33c996dc1 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -67,12 +67,13 @@ def load_windrose(): ) # Pour this into a parallel computing interface + parallel_interface = "concurrent" fi_aep_parallel = ParallelComputingInterface( fi=fi_aep, max_workers=max_workers, n_wind_direction_splits=max_workers, n_wind_speed_splits=1, - use_mpi4py=False, + interface=parallel_interface, print_timings=True, ) @@ -113,7 +114,7 @@ def load_windrose(): max_workers=max_workers, n_wind_direction_splits=max_workers, n_wind_speed_splits=1, - use_mpi4py=False, + interface=parallel_interface, print_timings=True, ) diff --git a/examples/14_compare_yaw_optimizers.py b/examples/14_compare_yaw_optimizers.py index 9e22f6872..1c4e29c31 100644 --- a/examples/14_compare_yaw_optimizers.py +++ b/examples/14_compare_yaw_optimizers.py @@ -18,6 +18,9 @@ import numpy as np from floris.tools import FlorisInterface +from floris.tools.optimization.yaw_optimization.yaw_optimizer_geometric import ( + YawOptimizationGeometric, +) from floris.tools.optimization.yaw_optimization.yaw_optimizer_scipy import YawOptimizationScipy from floris.tools.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR @@ -31,6 +34,13 @@ We then perform the optimization using both methods. Finally, we compare the time it took to find the optimal angles and plot the optimal yaw angles and resulting wind farm powers. + +The example now also compares the Geometric Yaw optimizer, which is fast +a method to find approximately optimal yaw angles based on the wind farm geometry. Its +main use case is for coupled layout and yaw optimization. +see floris.tools.optimization.yaw_optimization.yaw_optimizer_geometric.py and the paper online +at https://wes.copernicus.org/preprints/wes-2023-1/. See also example 16c. + """ # Load the default example floris object @@ -58,17 +68,29 @@ df_opt_sr = yaw_opt_sr.optimize() time_sr = timerpc() - start_time +print("Performing optimizations with Geometric Yaw...") +start_time = timerpc() +yaw_opt_geo = YawOptimizationGeometric(fi) +df_opt_geo = yaw_opt_geo.optimize() +time_geo = timerpc() - start_time + + + # Print time spent -print("\n Time spent, Serial Refine: {:.2f} s.".format(time_sr)) +print("\n Time spent, Geometric Yaw: {:.2f} s.".format(time_geo)) +print(" Time spent, Serial Refine: {:.2f} s.".format(time_sr)) print(" Time spent, SciPy (SLSQP): {:.2f} s.\n".format(time_scipy)) # Split out the turbine results +yaw_angles_opt_geo = np.vstack(df_opt_geo.yaw_angles_opt) yaw_angles_opt_sr = np.vstack(df_opt_sr.yaw_angles_opt) yaw_angles_opt_scipy = np.vstack(df_opt_scipy.yaw_angles_opt) + # Yaw results for t in range(3): fig, ax = plt.subplots() + ax.plot(df_opt_geo.wind_direction, yaw_angles_opt_geo[:, t],color='m',label='Geometric') ax.plot(df_opt_sr.wind_direction, yaw_angles_opt_sr[:, t],color='r',label='Serial Refine') ax.plot(df_opt_scipy.wind_direction, yaw_angles_opt_scipy[:, t],'--', color='g', label='SciPy') ax.grid(True) @@ -77,7 +99,15 @@ ax.grid(True) ax.set_title("Turbine {:d}".format(t)) -# Power results +# Power results ============== + +# Before plotting results, need to compute values for GEOOPT since it doesn't compute +# power within the optimization +yaw_angles_opt_geo_3d = np.expand_dims(yaw_angles_opt_geo, axis=1) +fi.calculate_wake(yaw_angles=yaw_angles_opt_geo_3d) +geo_farm_power = fi.get_farm_power().squeeze() + + fig, ax = plt.subplots() ax.plot( df_opt_sr.wind_direction, @@ -85,6 +115,12 @@ color='k', label='Baseline' ) +ax.plot( + df_opt_geo.wind_direction, + geo_farm_power, + color='m', + label='Optimized, Gemoetric' +) ax.plot( df_opt_sr.wind_direction, df_opt_sr.farm_power_opt, @@ -103,4 +139,75 @@ ax.legend() ax.grid(True) +# Finally, compare the overall the power gains + +fig, ax = plt.subplots() + +ax.plot( + df_opt_geo.wind_direction, + geo_farm_power - df_opt_sr.farm_power_baseline, + color='m', + label='Optimized, Gemoetric' +) +ax.plot( + df_opt_sr.wind_direction, + df_opt_sr.farm_power_opt - df_opt_sr.farm_power_baseline, + color='r', + label='Optimized, Serial Refine' +) +ax.plot( + df_opt_scipy.wind_direction, + df_opt_scipy.farm_power_opt - df_opt_scipy.farm_power_baseline, + '--', + color='g', + label='Optimized, SciPy' +) +ax.set_ylabel('Increase in Wind Farm Power (W)') +ax.set_xlabel('Wind Direction (deg)') +ax.legend() +ax.grid(True) + + +# Finally, make a quick bar plot comparing nomimal power and nomimal uplift +total_power_uplift_geo = np.sum(geo_farm_power - df_opt_sr.farm_power_baseline) +total_power_uplift_sr = np.sum(df_opt_sr.farm_power_opt - df_opt_sr.farm_power_baseline) +total_power_uplift_scipy = np.sum(df_opt_scipy.farm_power_opt - df_opt_scipy.farm_power_baseline) + +# Plot on the left subplot a barplot comparing the uplift normalized to scipy and on the right +# subplot a barplot of total time normalzed to scipy +fig, axarr = plt.subplots(1,2,figsize=(10,5)) + +ax = axarr[0] +ax.bar( + [0, 1, 2], + [ + total_power_uplift_geo / total_power_uplift_scipy, + total_power_uplift_sr / total_power_uplift_scipy, + 1.0, + ], + color=['m', 'r', 'g'], +) +ax.set_xticks([0, 1, 2]) +ax.set_xticklabels(['Geometric', 'Serial Refine', 'SciPy']) +ax.set_ylabel('Normalized Power Gain') +ax.grid(True) + +ax = axarr[1] +ax.bar( + [0, 1, 2], + [ + time_geo / time_scipy, + time_sr / time_scipy, + 1.0, + ], + color=['m', 'r', 'g'], +) +ax.set_xticks([0, 1, 2]) +ax.set_xticklabels(['Geometric', 'Serial Refine', 'SciPy']) +ax.set_ylabel('Normalized Computation Time') +ax.grid(True) + +# Change to semi-logy +axarr[1].set_yscale('log') + plt.show() diff --git a/examples/15_optimize_layout.py b/examples/15_optimize_layout.py index 2e44fad31..68ff4a895 100644 --- a/examples/15_optimize_layout.py +++ b/examples/15_optimize_layout.py @@ -15,6 +15,7 @@ import os +import matplotlib.pyplot as plt import numpy as np from floris.tools import FlorisInterface @@ -85,3 +86,5 @@ f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' ) layout_opt.plot_layout_opt_results() + +plt.show() diff --git a/examples/16_heterogeneous_inflow.py b/examples/16_heterogeneous_inflow.py index 3f04d5bc4..3dedf05e7 100644 --- a/examples/16_heterogeneous_inflow.py +++ b/examples/16_heterogeneous_inflow.py @@ -70,17 +70,30 @@ # Create the plots fig, ax_list = plt.subplots(3, 1, figsize=(10, 8)) ax_list = ax_list.flatten() -visualize_cut_plane(horizontal_plane_2d, ax=ax_list[0], title="Horizontal", color_bar=True) +visualize_cut_plane( + horizontal_plane_2d, + ax=ax_list[0], + title="Horizontal", + color_bar=True, + label_contours=True +) ax_list[0].set_xlabel('x') ax_list[0].set_ylabel('y') -visualize_cut_plane(y_plane_2d, ax=ax_list[1], title="Streamwise profile", color_bar=True) +visualize_cut_plane( + y_plane_2d, + ax=ax_list[1], + title="Streamwise profile", + color_bar=True, + label_contours=True +) ax_list[1].set_xlabel('x') ax_list[1].set_ylabel('z') visualize_cut_plane( cross_plane_2d, ax=ax_list[2], title="Spanwise profile at 500m downstream", - color_bar=True + color_bar=True, + label_contours=True ) ax_list[2].set_xlabel('y') ax_list[2].set_ylabel('z') @@ -136,7 +149,8 @@ horizontal_plane_3d, ax=ax_list[0], title="Horizontal", - color_bar=True + color_bar=True, + label_contours=True ) ax_list[0].set_xlabel('x') ax_list[0].set_ylabel('y') @@ -144,7 +158,8 @@ y_plane_3d, ax=ax_list[1], title="Streamwise profile", - color_bar=True + color_bar=True, + label_contours=True ) ax_list[1].set_xlabel('x') ax_list[1].set_ylabel('z') @@ -152,7 +167,8 @@ cross_plane_3d, ax=ax_list[2], title="Spanwise profile at 500m downstream", - color_bar=True + color_bar=True, + label_contours=True ) ax_list[2].set_xlabel('y') ax_list[2].set_ylabel('z') diff --git a/examples/16b_heterogenaity_multiple_ws_wd.py b/examples/16b_heterogeneity_multiple_ws_wd.py similarity index 100% rename from examples/16b_heterogenaity_multiple_ws_wd.py rename to examples/16b_heterogeneity_multiple_ws_wd.py diff --git a/examples/16c_optimize_layout_with_heterogeneity.py b/examples/16c_optimize_layout_with_heterogeneity.py new file mode 100644 index 000000000..ca27e3d7f --- /dev/null +++ b/examples/16c_optimize_layout_with_heterogeneity.py @@ -0,0 +1,171 @@ +# Copyright 2022 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +import os + +import matplotlib.pyplot as plt +import numpy as np + +from floris.tools import FlorisInterface +from floris.tools.optimization.layout_optimization.layout_optimization_scipy import ( + LayoutOptimizationScipy, +) + + +""" +This example shows a layout optimization using the geometric yaw option. It +combines elements of examples 15 (layout optimization) and 16 (heterogeneous +inflow) for demonstrative purposes. If you haven't yet run those examples, +we recommend you try them first. + +Heterogeneity in the inflow provides the necessary driver for coupled yaw +and layout optimization to be worthwhile. First, a layout optimization is +run without coupled yaw optimization; then a coupled optimization is run to +show the benefits of coupled optimization when flows are heterogeneous. +""" + +# Initialize the FLORIS interface fi +file_dir = os.path.dirname(os.path.abspath(__file__)) +fi = FlorisInterface('inputs/gch.yaml') + +# Setup 2 wind directions (due east and due west) +# and 1 wind speed with uniform probability +wind_directions = [270., 90.] +n_wds = len(wind_directions) +wind_speeds = [8.0] +# Shape frequency distribution to match number of wind directions and wind speeds +freq = np.ones((len(wind_directions), len(wind_speeds))) +freq = freq / freq.sum() + +# The boundaries for the turbines, specified as vertices +D = 126.0 # rotor diameter for the NREL 5MW +size_D = 12 +boundaries = [ + (0.0, 0.0), + (size_D * D, 0.0), + (size_D * D, 0.1), + (0.0, 0.1), + (0.0, 0.0) +] + +# Set turbine locations to 4 turbines at corners of the rectangle +# (optimal without flow heterogeneity) +layout_x = [0.1, 0.3*size_D*D, 0.6*size_D*D] +layout_y = [0, 0, 0] + +# Generate exaggerated heterogeneous inflow (same for all wind directions) +speed_multipliers = np.repeat(np.array([0.5, 1.0, 0.5, 1.0])[None,:], n_wds, axis=0) +x_locs = [0, size_D * D, 0, size_D * D] +y_locs = [-D, -D, D, D] + +# Create the configuration dictionary to be used for the heterogeneous inflow. +heterogenous_inflow_config = { + 'speed_multipliers': speed_multipliers, + 'x': x_locs, + 'y': y_locs, +} + +fi.reinitialize( + layout_x=layout_x, + layout_y=layout_y, + wind_directions=wind_directions, + wind_speeds=wind_speeds, + heterogenous_inflow_config=heterogenous_inflow_config +) + +# Setup and solve the layout optimization problem without heterogeneity +maxiter = 100 +layout_opt = LayoutOptimizationScipy( + fi, + boundaries, + freq=freq, + min_dist=2*D, + optOptions={"maxiter":maxiter} +) + +# Run the optimization +np.random.seed(0) +sol = layout_opt.optimize() + +# Get the resulting improvement in AEP +print('... calcuating improvement in AEP') +fi.calculate_wake() +base_aep = fi.get_farm_AEP(freq=freq) / 1e6 +fi.reinitialize(layout_x=sol[0], layout_y=sol[1]) +fi.calculate_wake() +opt_aep = fi.get_farm_AEP(freq=freq) / 1e6 +percent_gain = 100 * (opt_aep - base_aep) / base_aep + +# Print and plot the results +print(f'Optimal layout: {sol}') +print( + f'Optimal layout improves AEP by {percent_gain:.1f}% ' + f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' +) +layout_opt.plot_layout_opt_results() +ax = plt.gca() +fig = plt.gcf() +sm = ax.tricontourf(x_locs, y_locs, speed_multipliers[0], cmap="coolwarm") +fig.colorbar(sm, ax=ax, label="Speed multiplier") +ax.legend(["Initial layout", "Optimized layout", "Optimization boundary"]) +ax.set_title("Geometric yaw disabled") + + +# Rerun the layout optimization with geometric yaw enabled +print("\nReoptimizing with geometric yaw enabled.") +fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +layout_opt = LayoutOptimizationScipy( + fi, + boundaries, + freq=freq, + min_dist=2*D, + enable_geometric_yaw=True, + optOptions={"maxiter":maxiter} +) + +# Run the optimization +np.random.seed(0) +sol = layout_opt.optimize() + +# Get the resulting improvement in AEP +print('... calcuating improvement in AEP') +fi.calculate_wake() +base_aep = fi.get_farm_AEP(freq=freq) / 1e6 +fi.reinitialize(layout_x=sol[0], layout_y=sol[1]) +fi.calculate_wake() +opt_aep = fi.get_farm_AEP(freq=freq, yaw_angles=layout_opt.yaw_angles) / 1e6 +percent_gain = 100 * (opt_aep - base_aep) / base_aep + +# Print and plot the results +print(f'Optimal layout: {sol}') +print( + f'Optimal layout improves AEP by {percent_gain:.1f}% ' + f'from {base_aep:.1f} MWh to {opt_aep:.1f} MWh' +) +layout_opt.plot_layout_opt_results() +ax = plt.gca() +fig = plt.gcf() +sm = ax.tricontourf(x_locs, y_locs, speed_multipliers[0], cmap="coolwarm") +fig.colorbar(sm, ax=ax, label="Speed multiplier") +ax.legend(["Initial layout", "Optimized layout", "Optimization boundary"]) +ax.set_title("Geometric yaw enabled") + +print( + 'Turbine geometric yaw angles for wind direction {0:.2f}'.format(wind_directions[1])\ + +' and wind speed {0:.2f} m/s:'.format(wind_speeds[0]), + f'{layout_opt.yaw_angles[1,0,:]}' +) + +plt.show() diff --git a/examples/18_check_turbine.py b/examples/18_check_turbine.py index ec9d9b20e..e71f321ff 100644 --- a/examples/18_check_turbine.py +++ b/examples/18_check_turbine.py @@ -42,6 +42,8 @@ turbines = os.listdir('../floris/turbine_library') turbines = [t for t in turbines if 'yaml' in t] turbines = [t.strip('.yaml') for t in turbines] +# Remove multi-dimensional Cp/Ct turbine definitions as they require different handling +turbines = [i for i in turbines if ('multi_dim' not in i)] # Declare a set of figures for comparing cp and ct across models fig_cp_ct, axarr_cp_ct = plt.subplots(2,1,sharex=True,figsize=(10,10)) diff --git a/examples/25_tilt_driven_vertical_wake_deflection.py b/examples/25_tilt_driven_vertical_wake_deflection.py index f7897fe53..1725e4134 100644 --- a/examples/25_tilt_driven_vertical_wake_deflection.py +++ b/examples/25_tilt_driven_vertical_wake_deflection.py @@ -41,7 +41,7 @@ # Figure settings x_bounds = [-500, 3000] y_bounds = [-250, 250] -z_bounds = [0, 500] +z_bounds = [0.001, 500] cross_plane_locations = [10, 1200, 2500] horizontal_plane_location=90.0 diff --git a/examples/26_empirical_gauss_velocity_deficit_parameters.py b/examples/26_empirical_gauss_velocity_deficit_parameters.py index a34d4cc61..b2787059c 100644 --- a/examples/26_empirical_gauss_velocity_deficit_parameters.py +++ b/examples/26_empirical_gauss_velocity_deficit_parameters.py @@ -42,7 +42,7 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): # Using the FlorisInterface functions, get 2D slices. x_bounds = [-500, 3000] y_bounds = [-250, 250] - z_bounds = [0, 500] + z_bounds = [0.001, 500] cross_plane_locations = [10, 1200, 2500] horizontal_plane_location = 90.0 streamwise_plane_location = 0.0 @@ -150,7 +150,7 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): # Increase the base recovery rate fi_dict_mod = copy.deepcopy(fi_dict) fi_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ - ['wake_expansion_rates'] = [0.02, 0.01] + ['wake_expansion_rates'] = [0.03, 0.015] fi = FlorisInterface(fi_dict_mod) fi.reinitialize( wind_speeds=[8.0], diff --git a/examples/27_empirical_gauss_deflection_parameters.py b/examples/27_empirical_gauss_deflection_parameters.py index 2ddb8a647..5e453a7ad 100644 --- a/examples/27_empirical_gauss_deflection_parameters.py +++ b/examples/27_empirical_gauss_deflection_parameters.py @@ -39,12 +39,14 @@ yaw_angles = np.array(first_three_yaw_angles + [0.]*(num_in_row-3))\ [None, None, :] +print("Turbine yaw angles (degrees): ", yaw_angles[0,0,:]) + # Define function for visualizing wakes def generate_wake_visualization(fi, title=None): # Using the FlorisInterface functions, get 2D slices. x_bounds = [-500, 3000] y_bounds = [-250, 250] - z_bounds = [0, 500] + z_bounds = [0.001, 500] cross_plane_locations = [10, 1200, 2500] horizontal_plane_location = 90.0 streamwise_plane_location = 0.0 @@ -153,8 +155,6 @@ def generate_wake_visualization(fi, title=None): # Increase the maximum deflection attained fi_dict_mod = copy.deepcopy(fi_dict) -print(fi_dict_mod['wake']['wake_deflection_parameters']['empirical_gauss']) - fi_dict_mod['wake']['wake_deflection_parameters']['empirical_gauss']\ ['horizontal_deflection_gain_D'] = 5.0 diff --git a/examples/29_floating_vs_fixedbottom_farm.py b/examples/29_floating_vs_fixedbottom_farm.py new file mode 100644 index 000000000..d7c3dc29d --- /dev/null +++ b/examples/29_floating_vs_fixedbottom_farm.py @@ -0,0 +1,147 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy.interpolate import NearestNDInterpolator + +import floris.tools.visualization as wakeviz +from floris.tools import FlorisInterface + + +""" +This example demonstrates the impact of floating on turbine power and thurst +and wake behavior. A floating turbine in FLORIS is defined by including a +`floating_tilt_table` in the turbine input yaml which sets the steady tilt +angle of the turbine based on wind speed. This tilt angle is computed for each +turbine based on effective velocity. This tilt angle is then passed on +to the respective wake model. + +The value of the parameter ref_tilt_cp_ct is the value of tilt at which the +ct/cp curves have been defined. + +With floating_correct_cp_ct_for_tilt True, the difference between the current +tilt as interpolated from the floating tilt table is used to scale the turbine +power and thrust. + +In the example below, a 20-turbine, gridded wind farm is simulated using +the Empirical Gaussian wake model to show the effects of floating turbines on +both turbine power and wake development. + +fi_fixed: Fixed bottom turbine (no tilt variation with wind speed) +fi_floating: Floating turbine (tilt varies with wind speed) +""" + +# Declare the Floris Interface for fixed bottom, provide layout +fi_fixed = FlorisInterface("inputs_floating/emgauss_fixed.yaml") +fi_floating = FlorisInterface("inputs_floating/emgauss_floating.yaml") +x, y = np.meshgrid(np.linspace(0, 4*630., 5), np.linspace(0, 3*630., 4)) +for fi in [fi_fixed, fi_floating]: + fi.reinitialize(layout_x=x.flatten(), layout_y=y.flatten()) + +# Compute a single wind speed and direction, power and wakes +for fi in [fi_fixed, fi_floating]: + fi.reinitialize( + layout_x=x.flatten(), + layout_y=y.flatten(), + wind_speeds=[10], + wind_directions=[270] + ) + fi.calculate_wake() + +powers_fixed = fi_fixed.get_turbine_powers() +powers_floating = fi_floating.get_turbine_powers() +power_difference = powers_floating - powers_fixed + +# Show the power differences +fig, ax = plt.subplots() +ax.set_aspect('equal', adjustable='box') +sc = ax.scatter( + x.flatten(), + y.flatten(), + c=power_difference.flatten()/1000, + cmap="PuOr", + vmin=-30, + vmax=30, + s=200, +) +ax.set_xlabel("x coordinate [m]") +ax.set_ylabel("y coordinate [m]") +ax.set_title("Power increase due to floating for each turbine.") +plt.colorbar(sc, label="Increase (kW)") + +print("Power increase from floating over farm (10m/s, 270deg winds): {0:.2f} kW".\ + format(power_difference.sum()/1000)) + +# Visualize flows (see also 02_visualizations.py) +horizontal_planes = [] +y_planes = [] +for fi in [fi_fixed, fi_floating]: + horizontal_planes.append( + fi.calculate_horizontal_plane( + x_resolution=200, + y_resolution=100, + height=90.0, + ) + ) + y_planes.append( + fi.calculate_y_plane( + x_resolution=200, + z_resolution=100, + crossstream_dist=0.0, + ) + ) + +# Create the plots +fig, ax_list = plt.subplots(2, 1, figsize=(10, 8)) +ax_list = ax_list.flatten() +wakeviz.visualize_cut_plane(horizontal_planes[0], ax=ax_list[0], title="Horizontal") +wakeviz.visualize_cut_plane(y_planes[0], ax=ax_list[1], title="Streamwise profile") +fig.suptitle("Fixed-bottom farm") + +fig, ax_list = plt.subplots(2, 1, figsize=(10, 8)) +ax_list = ax_list.flatten() +wakeviz.visualize_cut_plane(horizontal_planes[1], ax=ax_list[0], title="Horizontal") +wakeviz.visualize_cut_plane(y_planes[1], ax=ax_list[1], title="Streamwise profile") +fig.suptitle("Floating farm") + +# Compute AEP (see 07_calc_aep_from_rose.py for details) +df_wr = pd.read_csv("inputs/wind_rose.csv") +wd_array = np.array(df_wr["wd"].unique(), dtype=float) +ws_array = np.array(df_wr["ws"].unique(), dtype=float) + +wd_grid, ws_grid = np.meshgrid(wd_array, ws_array, indexing="ij") +freq_interp = NearestNDInterpolator(df_wr[["wd", "ws"]], df_wr["freq_val"]) +freq = freq_interp(wd_grid, ws_grid) +freq = freq / np.sum(freq) + +for fi in [fi_fixed, fi_floating]: + fi.reinitialize( + wind_directions=wd_array, + wind_speeds=ws_array, + ) + +# Compute the AEP +aep_fixed = fi_fixed.get_farm_AEP(freq=freq) +aep_floating = fi_floating.get_farm_AEP(freq=freq) +print("Farm AEP (fixed bottom): {:.3f} GWh".format(aep_fixed / 1.0e9)) +print("Farm AEP (floating): {:.3f} GWh".format(aep_floating / 1.0e9)) +print("Floating AEP increase: {0:.3f} GWh ({1:.2f}%)".\ + format((aep_floating - aep_fixed) / 1.0e9, + (aep_floating - aep_fixed)/aep_fixed*100 + ) +) + +plt.show() diff --git a/examples/30_multi_dimensional_cp_ct.py b/examples/30_multi_dimensional_cp_ct.py new file mode 100644 index 000000000..2d2303018 --- /dev/null +++ b/examples/30_multi_dimensional_cp_ct.py @@ -0,0 +1,104 @@ +# Copyright 2023 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +import numpy as np + +from floris.tools import FlorisInterface + + +""" +This example follows the same setup as example 01 to createa a FLORIS instance and: +1) Makes a two-turbine layout +2) Demonstrates single ws/wd simulations +3) Demonstrates mulitple ws/wd simulations + +with the modification of using a turbine definition that has a multi-dimensional Cp/Ct table. + +In the input file `gch_multi_dim_cp_ct.yaml`, the turbine_type points to a turbine definition, +iea_15MW_floating_multi_dim_cp_ct.yaml located in the turbine_library, +that supplies a multi-dimensional Cp/Ct data file in the form of a .csv file. This .csv file +contains two additional conditions to define Cp/Ct values for: Tp for wave period, and Hs for wave +height. For every combination of Tp and Hs defined, a Cp/Ct/Wind speed table of values is also +defined. It is required for this .csv file to have the last 3 columns be ws, Cp, and Ct. In order +for this table to be used, the flag 'multi_dimensional_cp_ct' must be present and set to true in +the turbine definition. Also of note is the 'velocity_model' must be set to 'multidim_cp_ct' in +the main input file. With both of these values provided, the solver will downselect to use the +interpolant defined at the closest conditions. The user must supply these conditions in the +main input file under the 'flow_field' section, e.g.: + +NOTE: The multi-dimensional Cp/Ct data used in this example is fictional for the purposes of +facilitating this example. The Cp/Ct values for the different wave conditions are scaled +values of the original Cp/Ct data for the IEA 15MW turbine. + +flow_field: + multidim_conditions: + Tp: 2.5 + Hs: 3.01 + +The solver will then use the nearest-neighbor interpolant. These conditions are currently global +and used to select the interpolant at each turbine. + +Also note in the example below that there is a specific method for computing powers when +using turbines with multi-dimensional Cp/Ct data under FlorisInterface, called +'get_turbine_powers_multidim'. The normal 'get_turbine_powers' method will not work. +""" + +# Initialize FLORIS with the given input file via FlorisInterface. +fi = FlorisInterface("inputs/gch_multi_dim_cp_ct.yaml") + +# Convert to a simple two turbine layout +fi.reinitialize(layout_x=[0., 500.], layout_y=[0., 0.]) + +# Single wind speed and wind direction +print('\n========================= Single Wind Direction and Wind Speed =========================') + +# Get the turbine powers assuming 1 wind speed and 1 wind direction +fi.reinitialize(wind_directions=[270.], wind_speeds=[8.0]) + +# Set the yaw angles to 0 +yaw_angles = np.zeros([1,1,2]) # 1 wind direction, 1 wind speed, 2 turbines +fi.calculate_wake(yaw_angles=yaw_angles) + +# Get the turbine powers +turbine_powers = fi.get_turbine_powers_multidim()/1000. +print('The turbine power matrix should be of dimensions 1 WD X 1 WS X 2 Turbines') +print(turbine_powers) +print("Shape: ",turbine_powers.shape) + +# Single wind speed and multiple wind directions +print('\n========================= Single Wind Direction and Multiple Wind Speeds ===============') + + +wind_speeds = np.array([8.0, 9.0, 10.0]) +fi.reinitialize(wind_speeds=wind_speeds) +yaw_angles = np.zeros([1,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines +fi.calculate_wake(yaw_angles=yaw_angles) +turbine_powers = fi.get_turbine_powers_multidim()/1000. +print('The turbine power matrix should be of dimensions 1 WD X 3 WS X 2 Turbines') +print(turbine_powers) +print("Shape: ",turbine_powers.shape) + +# Multiple wind speeds and multiple wind directions +print('\n========================= Multiple Wind Directions and Multiple Wind Speeds ============') + +wind_directions = np.array([260., 270., 280.]) +wind_speeds = np.array([8.0, 9.0, 10.0]) +fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) +yaw_angles = np.zeros([1,3,2]) # 1 wind direction, 3 wind speeds, 2 turbines +fi.calculate_wake(yaw_angles=yaw_angles) +turbine_powers = fi.get_turbine_powers_multidim()/1000. +print('The turbine power matrix should be of dimensions 3 WD X 3 WS X 2 Turbines') +print(turbine_powers) +print("Shape: ",turbine_powers.shape) diff --git a/examples/31_multi_dimensional_cp_ct_2Hs.py b/examples/31_multi_dimensional_cp_ct_2Hs.py new file mode 100644 index 000000000..6bbc31d6d --- /dev/null +++ b/examples/31_multi_dimensional_cp_ct_2Hs.py @@ -0,0 +1,76 @@ +# Copyright 2023 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +import matplotlib.pyplot as plt +import numpy as np + +from floris.tools import FlorisInterface + + +""" +This example follows after example 30 but shows the effect of changing the Hs setting. + +NOTE: The multi-dimensional Cp/Ct data used in this example is fictional for the purposes of +facilitating this example. The Cp/Ct values for the different wave conditions are scaled +values of the original Cp/Ct data for the IEA 15MW turbine. +""" + +# Initialize FLORIS with the given input file via FlorisInterface. +fi = FlorisInterface("inputs/gch_multi_dim_cp_ct.yaml") + +# Make a second FLORIS interface with a different setting for Hs. +# Note the multi-cp-ct file (iea_15MW_multi_dim_Tp_Hs.csv) +# for the turbine model iea_15MW_floating_multi_dim_cp_ct.yaml +# Defines Hs at 1 and 5. +# The value in gch_multi_dim_cp_ct.yaml is 3.01 which will map +# to 5 as the nearer value, so we set the other case to 1 +# for contrast. +fi_dict_mod = fi.floris.as_dict() +fi_dict_mod['flow_field']['multidim_conditions']['Hs'] = 1.0 +fi_hs_1 = FlorisInterface(fi_dict_mod) + +# Set both cases to 3 turbine layout +fi.reinitialize(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) +fi_hs_1.reinitialize(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) + +# Use a sweep of wind speeds +wind_speeds = np.arange(5,20,1.) +fi.reinitialize(wind_directions=[270.], wind_speeds=wind_speeds) +fi_hs_1.reinitialize(wind_directions=[270.], wind_speeds=wind_speeds) + +# Calculate wakes with baseline yaw +fi.calculate_wake() +fi_hs_1.calculate_wake() + +# Collect the turbine powers in kW +turbine_powers = fi.get_turbine_powers_multidim()/1000. +turbine_powers_hs_1 = fi_hs_1.get_turbine_powers_multidim()/1000. + +# Plot the power in each case and the difference in power +fig, axarr = plt.subplots(1,3,sharex=True,figsize=(12,4)) + +for t_idx in range(3): + ax = axarr[t_idx] + ax.plot(wind_speeds, turbine_powers[0,:,t_idx], color='k', label='Hs=3.1 (5)') + ax.plot(wind_speeds, turbine_powers_hs_1[0,:,t_idx], color='r', label='Hs=1.0') + ax.grid(True) + ax.set_xlabel('Wind Speed (m/s)') + ax.set_title(f'Turbine {t_idx}') + +axarr[0].set_ylabel('Power (kW)') +axarr[0].legend() +fig.suptitle('Power of each turbine') + +plt.show() diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index dab5c7940..f984f421d 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -65,7 +65,7 @@ wake: empirical_gauss: horizontal_deflection_gain_D: 3.0 vertical_deflection_gain_D: -1 - deflection_rate: 15 + deflection_rate: 30 mixing_gain_deflection: 0.0 yaw_added_mixing_gain: 0.0 @@ -88,8 +88,8 @@ wake: we: 0.05 empirical_gauss: wake_expansion_rates: - - 0.01 - - 0.005 + - 0.023 + - 0.008 breakpoints_D: - 10 sigma_0_D: 0.28 diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index c30c35c3b..220fafeac 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -134,6 +134,15 @@ flow_field: # The wind veer as a constant value for all points in the grid. wind_veer: 0.0 + ### + # The conditions that are specified for use with the multi-dimensional Cp/Ct capbility. + # These conditions are external to FLORIS and specified by the user. They are used internally + # through a nearest-neighbor selection process to choose the correct Cp/Ct interpolants + # to use. These conditions are only used with the ``multidim_cp_ct`` velocity deficit model. + multidim_conditions: + Tp: 2.5 + Hs: 3.01 + ### # Configure the wake model. wake: diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml new file mode 100644 index 000000000..8709fbcc7 --- /dev/null +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -0,0 +1,92 @@ + +name: GCH multi dimensional Cp/Ct +description: Three turbines using GCH model +floris_version: v3.0.0 + +logging: + console: + enable: true + level: WARNING + file: + enable: false + level: WARNING + +solver: + type: turbine_grid + turbine_grid_points: 3 + +farm: + layout_x: + - 0.0 + - 630.0 + - 1260.0 + layout_y: + - 0.0 + - 0.0 + - 0.0 + turbine_type: + - iea_15MW_floating_multi_dim_cp_ct + +flow_field: + multidim_conditions: + Tp: 2.5 + Hs: 3.01 + air_density: 1.225 + reference_wind_height: -1 # -1 is code for use the hub height + turbulence_intensity: 0.06 + wind_directions: + - 270.0 + wind_shear: 0.12 + wind_speeds: + - 8.0 + wind_veer: 0.0 + +wake: + model_strings: + combination_model: sosfs + deflection_model: gauss + turbulence_model: crespo_hernandez + velocity_model: multidim_cp_ct + + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + + wake_deflection_parameters: + gauss: + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + jimenez: + ad: 0.0 + bd: 0.0 + kd: 0.05 + + wake_velocity_parameters: + cc: + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + multidim_cp_ct: + alpha: 0.58 + beta: 0.077 + ka: 0.38 + kb: 0.004 + jensen: + we: 0.05 + + wake_turbulence_parameters: + crespo_hernandez: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml new file mode 100644 index 000000000..9d0b23960 --- /dev/null +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -0,0 +1,105 @@ + +name: Emperical Gaussian +description: Example of single fixed-bottom turbine +floris_version: v3.x + +logging: + console: + enable: true + level: WARNING + file: + enable: false + level: WARNING + +solver: + type: turbine_grid + turbine_grid_points: 3 + +farm: + layout_x: + - 0.0 + - 630.0 + - 1260.0 + layout_y: + - 0.0 + - 0.0 + - 0.0 + turbine_type: + - !include turbine_files/nrel_5MW_fixed.yaml + +flow_field: + air_density: 1.225 + reference_wind_height: -1 # -1 is code for use the hub height + turbulence_intensity: 0.06 + wind_directions: + - 270.0 + wind_shear: 0.12 + wind_speeds: + - 8.0 + wind_veer: 0.0 + +wake: + model_strings: + combination_model: sosfs + deflection_model: empirical_gauss + turbulence_model: wake_induced_mixing + velocity_model: empirical_gauss + + enable_secondary_steering: false + enable_yaw_added_recovery: true + enable_transverse_velocities: false + + wake_deflection_parameters: + gauss: + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + jimenez: + ad: 0.0 + bd: 0.0 + kd: 0.05 + empirical_gauss: + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 30 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + + wake_velocity_parameters: + cc: + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + gauss: + alpha: 0.58 + beta: 0.077 + ka: 0.38 + kb: 0.004 + jensen: + we: 0.05 + empirical_gauss: + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + wake_turbulence_parameters: + crespo_hernandez: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + wake_induced_mixing: + atmospheric_ti_gain: 0.0 diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml new file mode 100644 index 000000000..1fd66d217 --- /dev/null +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -0,0 +1,105 @@ + +name: Emperical Gaussian +description: Example of single floating turbine +floris_version: v3.x + +logging: + console: + enable: true + level: WARNING + file: + enable: false + level: WARNING + +solver: + type: turbine_grid + turbine_grid_points: 3 + +farm: + layout_x: + - 0.0 + - 630.0 + - 1260.0 + layout_y: + - 0.0 + - 0.0 + - 0.0 + turbine_type: + - !include turbine_files/nrel_5MW_floating.yaml + +flow_field: + air_density: 1.225 + reference_wind_height: -1 # -1 is code for use the hub height + turbulence_intensity: 0.06 + wind_directions: + - 270.0 + wind_shear: 0.12 + wind_speeds: + - 8.0 + wind_veer: 0.0 + +wake: + model_strings: + combination_model: sosfs + deflection_model: empirical_gauss + turbulence_model: wake_induced_mixing + velocity_model: empirical_gauss + + enable_secondary_steering: false + enable_yaw_added_recovery: true + enable_transverse_velocities: false + + wake_deflection_parameters: + gauss: + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + jimenez: + ad: 0.0 + bd: 0.0 + kd: 0.05 + empirical_gauss: + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 30 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + + wake_velocity_parameters: + cc: + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + gauss: + alpha: 0.58 + beta: 0.077 + ka: 0.38 + kb: 0.004 + jensen: + we: 0.05 + empirical_gauss: + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + wake_turbulence_parameters: + crespo_hernandez: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + wake_induced_mixing: + atmospheric_ti_gain: 0.0 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index 1bfae562d..dfb4e3155 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -61,7 +61,7 @@ wake: empirical_gauss: horizontal_deflection_gain_D: 3.0 vertical_deflection_gain_D: -1 - deflection_rate: 15 + deflection_rate: 30 mixing_gain_deflection: 0.0 yaw_added_mixing_gain: 0.0 @@ -84,8 +84,8 @@ wake: we: 0.05 empirical_gauss: wake_expansion_rates: - - 0.01 - - 0.005 + - 0.023 + - 0.008 breakpoints_D: - 10 sigma_0_D: 0.28 diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index 04cf30518..67be5dfd3 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -61,7 +61,7 @@ wake: empirical_gauss: horizontal_deflection_gain_D: 3.0 vertical_deflection_gain_D: -1 - deflection_rate: 15 + deflection_rate: 30 mixing_gain_deflection: 0.0 yaw_added_mixing_gain: 0.0 @@ -84,8 +84,8 @@ wake: we: 0.05 empirical_gauss: wake_expansion_rates: - - 0.01 - - 0.005 + - 0.023 + - 0.008 breakpoints_D: - 10 sigma_0_D: 0.28 diff --git a/floris/simulation/__init__.py b/floris/simulation/__init__.py index 12d30aab8..6da5c5ac5 100644 --- a/floris/simulation/__init__.py +++ b/floris/simulation/__init__.py @@ -37,7 +37,21 @@ import floris.logging_manager from .base import BaseClass, BaseModel, State -from .turbine import average_velocity, axial_induction, Ct, power, rotor_effective_velocity, Turbine +from .turbine import ( + average_velocity, + axial_induction, + compute_tilt_angles_for_floating_turbines, + Ct, + power, + rotor_effective_velocity, + TiltTable, + Turbine +) +from .turbine_multi_dim import ( + axial_induction_multidim, + Ct_multidim, + TurbineMultiDimensional +) from .farm import Farm from .grid import ( FlowFieldGrid, @@ -57,6 +71,7 @@ full_flow_sequential_solver, full_flow_turbopark_solver, sequential_solver, + sequential_multidim_solver, turbopark_solver, ) from .floris import Floris diff --git a/floris/simulation/farm.py b/floris/simulation/farm.py index 211032ce1..dc1435a88 100644 --- a/floris/simulation/farm.py +++ b/floris/simulation/farm.py @@ -24,6 +24,7 @@ BaseClass, State, Turbine, + TurbineMultiDimensional, ) from floris.simulation.turbine import compute_tilt_angles_for_floating_turbines from floris.type_dec import ( @@ -247,13 +248,22 @@ def construct_turbine_correct_cp_ct_for_tilt(self): ) def construct_turbine_map(self): - self.turbine_map = [Turbine.from_dict(turb) for turb in self.turbine_definitions] + if 'multi_dimensional_cp_ct' in self.turbine_definitions[0].keys() \ + and self.turbine_definitions[0]['multi_dimensional_cp_ct'] is True: + self.turbine_map = [ + TurbineMultiDimensional.from_dict(turb) for turb in self.turbine_definitions + ] + else: + self.turbine_map = [Turbine.from_dict(turb) for turb in self.turbine_definitions] def construct_turbine_fCts(self): self.turbine_fCts = { turb.turbine_type: turb.fCt_interp for turb in self.turbine_map } + def construct_multidim_turbine_fCts(self): + self.turbine_fCts = [turb.fCt_interp for turb in self.turbine_map] + def construct_turbine_fTilts(self): self.turbine_fTilts = [(turb.turbine_type, turb.fTilt_interp) for turb in self.turbine_map] @@ -262,6 +272,9 @@ def construct_turbine_power_interps(self): turb.turbine_type: turb.power_interp for turb in self.turbine_map } + def construct_multidim_turbine_power_interps(self): + self.turbine_power_interps = [turb.power_interp for turb in self.turbine_map] + def construct_coordinates(self): self.coordinates = np.array([ Vec3([x, y, z]) for x, y, z in zip(self.layout_x, self.layout_y, self.hub_heights) @@ -279,6 +292,38 @@ def expand_farm_properties( sorted_coord_indices, axis=2 ) + if 'multi_dimensional_cp_ct' in self.turbine_definitions[0].keys() \ + and self.turbine_definitions[0]['multi_dimensional_cp_ct'] is True: + wd_dim = np.shape(template_shape)[0] + ws_dim = np.shape(template_shape)[1] + if wd_dim != 1 | ws_dim != 0: + self.turbine_fCts_sorted = np.take_along_axis( + np.reshape( + np.repeat(self.turbine_fCts, wd_dim * ws_dim), + np.shape(template_shape) + ), + sorted_coord_indices, + axis=2 + ) + self.turbine_power_interps_sorted = np.take_along_axis( + np.reshape( + np.repeat(self.turbine_power_interps, wd_dim * ws_dim), + np.shape(template_shape) + ), + sorted_coord_indices, + axis=2 + ) + else: + self.turbine_fCts_sorted = np.take_along_axis( + np.reshape(self.turbine_fCts, np.shape(template_shape)), + sorted_coord_indices, + axis=2 + ) + self.turbine_power_interps_sorted = np.take_along_axis( + np.reshape(self.turbine_power_interps, np.shape(template_shape)), + sorted_coord_indices, + axis=2 + ) self.rotor_diameters_sorted = np.take_along_axis( self.rotor_diameters * template_shape, sorted_coord_indices, @@ -355,6 +400,18 @@ def calculate_tilt_for_eff_velocities(self, rotor_effective_velocities): return tilt_angles def finalize(self, unsorted_indices): + if 'multi_dimensional_cp_ct' in self.turbine_definitions[0].keys() \ + and self.turbine_definitions[0]['multi_dimensional_cp_ct'] is True: + self.turbine_fCts = np.take_along_axis( + self.turbine_fCts_sorted, + unsorted_indices[:,:,:,0,0], + axis=2 + ) + self.turbine_power_interps = np.take_along_axis( + self.turbine_power_interps_sorted, + unsorted_indices[:,:,:,0,0], + axis=2 + ) self.yaw_angles = np.take_along_axis( self.yaw_angles_sorted, unsorted_indices[:,:,:,0,0], diff --git a/floris/simulation/floris.py b/floris/simulation/floris.py index 8ca8854e7..09722a2d7 100644 --- a/floris/simulation/floris.py +++ b/floris/simulation/floris.py @@ -34,6 +34,7 @@ full_flow_turbopark_solver, Grid, PointsGrid, + sequential_multidim_solver, sequential_solver, State, TurbineCubatureGrid, @@ -68,12 +69,26 @@ class Floris(BaseClass): def __attrs_post_init__(self) -> None: + # Configure logging + logging_manager.configure_console_log( + self.logging["console"]["enable"], + self.logging["console"]["level"], + ) + logging_manager.configure_file_log( + self.logging["file"]["enable"], + self.logging["file"]["level"], + ) + self.check_deprecated_inputs() # Initialize farm quanitities that depend on other objects self.farm.construct_turbine_map() - self.farm.construct_turbine_fCts() - self.farm.construct_turbine_power_interps() + if self.wake.model_strings['velocity_model'] == 'multidim_cp_ct': + self.farm.construct_multidim_turbine_fCts() + self.farm.construct_multidim_turbine_power_interps() + else: + self.farm.construct_turbine_fCts() + self.farm.construct_turbine_power_interps() self.farm.construct_hub_heights() self.farm.construct_rotor_diameters() self.farm.construct_turbine_TSRs() @@ -144,16 +159,6 @@ def __attrs_post_init__(self) -> None: self.grid.sorted_coord_indices ) - # Configure logging - logging_manager.configure_console_log( - self.logging["console"]["enable"], - self.logging["console"]["level"], - ) - logging_manager.configure_file_log( - self.logging["file"]["enable"], - self.logging["file"]["level"], - ) - def check_deprecated_inputs(self): """ This function should used when the FLORIS input file changes in order to provide @@ -243,6 +248,13 @@ def steady_state_atmospheric_condition(self): self.grid, self.wake ) + elif vel_model=="multidim_cp_ct": + sequential_multidim_solver( + self.farm, + self.flow_field, + self.grid, + self.wake + ) else: sequential_solver( self.farm, diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index a8dbf7393..2781d8c12 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -41,8 +41,9 @@ class FlowField(BaseClass): air_density: float = field(converter=float) turbulence_intensity: float = field(converter=float) reference_wind_height: float = field(converter=float) - time_series : bool = field(default=False) + time_series: bool = field(default=False) heterogenous_inflow_config: dict = field(default=None) + multidim_conditions: dict = field(default=None) n_wind_speeds: int = field(init=False) n_wind_directions: int = field(init=False) diff --git a/floris/simulation/grid.py b/floris/simulation/grid.py index 8e2325252..80f711f72 100644 --- a/floris/simulation/grid.py +++ b/floris/simulation/grid.py @@ -644,56 +644,6 @@ def set_grid(self) -> None: y_center_of_rotation=self.y_center_of_rotation, ) - # self.sorted_indices = self.x.argsort(axis=2) - # self.unsorted_indices = self.sorted_indices.argsort(axis=2) - - # Put the turbines into the final arrays in their sorted order - # self.x = np.take_along_axis(self.x, self.sorted_indices, axis=2) - # self.y = np.take_along_axis(self.y, self.sorted_indices, axis=2) - # self.z = np.take_along_axis(self.z, self.sorted_indices, axis=2) - - # def finalize(self): - # sorted_indices = self.x.argsort(axis=2) - # unsorted_indices = sorted_indices.argsort(axis=2) - - # # print(self.x) - - # x_coordinates, y_coordinates, _ = self.turbine_coordinates_array.T - - # x_center_of_rotation = (np.min(x_coordinates) + np.max(x_coordinates)) / 2 - # y_center_of_rotation = (np.min(y_coordinates) + np.max(y_coordinates)) / 2 - # # print(x_center_of_rotation) - # # print(y_center_of_rotation) - # # lkj - - # self.x = np.take_along_axis(self.x, self.unsorted_indices, axis=2) - # self.y = np.take_along_axis(self.y, self.unsorted_indices, axis=2) - # self.z = np.take_along_axis(self.z, self.unsorted_indices, axis=2) - # # print(self.x) - - # self.x, self.y, self.z = self._rotated_grid( - # -1 * self.wind_directions, - # (x_center_of_rotation, y_center_of_rotation) - # ) - # TODO figure out how to un-rotate grid for plotting after it has been solved - # pass - - # def _rotated_grid(self, angle, center_of_rotation): - # """ - # Rotate the discrete flow field grid. - # """ - # angle = ((angle - 270) % 360 + 360) % 360 - # # angle = np.reshape(angle, (len(angle), 1, 1)) - # xoffset = self.x - center_of_rotation[0] - # yoffset = self.y - center_of_rotation[1] - # rotated_x = ( - # xoffset * cosd(angle) - yoffset * sind(angle) + center_of_rotation[0] - # ) - # rotated_y = ( - # xoffset * sind(angle) + yoffset * cosd(angle) + center_of_rotation[1] - # ) - # return rotated_x, rotated_y, self.z - @define class PointsGrid(Grid): """ diff --git a/floris/simulation/solver.py b/floris/simulation/solver.py index 5f1767699..6e53a718a 100644 --- a/floris/simulation/solver.py +++ b/floris/simulation/solver.py @@ -27,6 +27,11 @@ TurbineGrid, ) from floris.simulation.turbine import average_velocity +from floris.simulation.turbine_multi_dim import ( + axial_induction_multidim, + Ct_multidim, + multidim_Ct_down_select, +) from floris.simulation.wake import WakeModelManager from floris.simulation.wake_deflection.empirical_gauss import yaw_added_wake_mixing from floris.simulation.wake_deflection.gauss import ( @@ -130,9 +135,9 @@ def sequential_solver( axial_induction_i = axial_induction_i[:, :, 0:1, None, None] turbulence_intensity_i = turbine_turbulence_intensity[:, :, i:i+1] yaw_angle_i = farm.yaw_angles_sorted[:, :, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[: ,:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[: ,:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[: ,:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, :, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, :, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, :, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -148,7 +153,8 @@ def sequential_solver( hub_height_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) effective_yaw_i += added_yaw @@ -161,7 +167,7 @@ def sequential_solver( turbulence_intensity_i, ct_i, rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) if model_manager.enable_transverse_velocities: @@ -177,7 +183,8 @@ def sequential_solver( yaw_angle_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) if model_manager.enable_yaw_added_recovery: @@ -204,7 +211,7 @@ def sequential_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args + **deficit_model_args, ) wake_field = model_manager.combination_model.function( @@ -217,7 +224,7 @@ def sequential_solver( grid.x_sorted, x_i, rotor_diameter_i, - axial_induction_i + axial_induction_i, ) # Calculate wake overlap for wake-added turbulence (WAT) @@ -232,9 +239,9 @@ def sequential_solver( ti_added = ( area_overlap * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * np.array(grid.x_sorted > x_i) - * np.array(np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * np.array(grid.x_sorted <= downstream_influence_length + x_i) + * (grid.x_sorted > x_i) + * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) + * (grid.x_sorted <= downstream_influence_length + x_i) ) # Combine turbine TIs with WAT @@ -291,7 +298,7 @@ def full_flow_sequential_solver( turbine_grid_farm.expand_farm_properties( turbine_grid_flow_field.n_wind_directions, turbine_grid_flow_field.n_wind_speeds, - turbine_grid.sorted_coord_indices + turbine_grid.sorted_coord_indices, ) turbine_grid_flow_field.initialize_velocity_field(turbine_grid) turbine_grid_farm.initialize(turbine_grid.sorted_indices) @@ -376,7 +383,8 @@ def full_flow_sequential_solver( hub_height_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) effective_yaw_i += added_yaw @@ -389,7 +397,7 @@ def full_flow_sequential_solver( turbulence_intensity_i, ct_i, rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) if model_manager.enable_transverse_velocities: @@ -405,7 +413,8 @@ def full_flow_sequential_solver( yaw_angle_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) # NOTE: exponential @@ -420,7 +429,7 @@ def full_flow_sequential_solver( ct_i, hub_height_i, rotor_diameter_i, - **deficit_model_args + **deficit_model_args, ) wake_field = model_manager.combination_model.function( @@ -476,10 +485,10 @@ def cc_solver( rotor_diameter_i = farm.rotor_diameters_sorted[: ,:, i:i+1, None, None] mask2 = ( - np.array(grid.x_sorted < x_i + 0.01) - * np.array(grid.x_sorted > x_i - 0.01) - * np.array(grid.y_sorted < y_i + 0.51 * rotor_diameter_i) - * np.array(grid.y_sorted > y_i - 0.51 * rotor_diameter_i) + (grid.x_sorted < x_i + 0.01) + * (grid.x_sorted > x_i - 0.01) + * (grid.y_sorted < y_i + 0.51 * rotor_diameter_i) + * (grid.y_sorted > y_i - 0.51 * rotor_diameter_i) ) turb_inflow_field = ( turb_inflow_field * ~mask2 @@ -536,8 +545,8 @@ def cc_solver( turbulence_intensity_i = turbine_turbulence_intensity[:, :, i:i+1] yaw_angle_i = farm.yaw_angles_sorted[:, :, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[: ,:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[: ,:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, :, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, :, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -554,6 +563,7 @@ def cc_solver( turb_Cts[:, :, i:i+1], TSR_i, axial_induction_i, + flow_field.wind_shear, scale=2.0, ) effective_yaw_i += added_yaw @@ -567,7 +577,7 @@ def cc_solver( turbulence_intensity_i, turb_Cts[:, :, i:i+1], rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) if model_manager.enable_transverse_velocities: @@ -584,7 +594,8 @@ def cc_solver( turb_Cts[:, :, i:i+1], TSR_i, axial_induction_i, - scale=2.0 + flow_field.wind_shear, + scale=2.0, ) if model_manager.enable_yaw_added_recovery: @@ -612,7 +623,7 @@ def cc_solver( farm.rotor_diameters_sorted[:, :, :, None, None], turb_u_wake, Ctmp, - **deficit_model_args + **deficit_model_args, ) wake_added_turbulence_intensity = model_manager.turbulence_model.function( @@ -635,16 +646,17 @@ def cc_solver( ti_added = ( area_overlap * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * np.array(grid.x_sorted > x_i) - * np.array(np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * np.array(grid.x_sorted <= downstream_influence_length + x_i) + * (grid.x_sorted > x_i) + * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) + * (grid.x_sorted <= downstream_influence_length + x_i) ) # Combine turbine TIs with WAT turbine_turbulence_intensity = np.maximum( - np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), + np.sqrt(ti_added ** 2 + ambient_turbulence_intensity ** 2), turbine_turbulence_intensity ) + flow_field.v_sorted += v_wake flow_field.w_sorted += w_wake flow_field.u_sorted = turb_inflow_field @@ -660,7 +672,7 @@ def full_flow_cc_solver( farm: Farm, flow_field: FlowField, flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager + model_manager: WakeModelManager, ) -> None: # Get the flow quantities and turbine performance turbine_grid_farm = copy.deepcopy(farm) @@ -692,7 +704,7 @@ def full_flow_cc_solver( turbine_grid_farm.expand_farm_properties( turbine_grid_flow_field.n_wind_directions, turbine_grid_flow_field.n_wind_speeds, - turbine_grid.sorted_coord_indices + turbine_grid.sorted_coord_indices, ) turbine_grid_flow_field.initialize_velocity_field(turbine_grid) turbine_grid_farm.initialize(turbine_grid.sorted_indices) @@ -764,9 +776,9 @@ def full_flow_cc_solver( turbulence_intensity_i = \ turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, :, i:i+1] yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, :, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[: ,:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[: ,:, i:i+1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[: ,:, i:i+1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, :, i:i+1, None, None] + rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, :, i:i+1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, :, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -783,7 +795,8 @@ def full_flow_cc_solver( turb_Cts[:, :, i:i+1], TSR_i, axial_induction_i, - scale=2.0 + flow_field.wind_shear, + scale=2.0, ) effective_yaw_i += added_yaw @@ -796,7 +809,7 @@ def full_flow_cc_solver( turbulence_intensity_i, turb_Cts[:, :, i:i+1], rotor_diameter_i, - **deflection_model_args + **deflection_model_args, ) if model_manager.enable_transverse_velocities: @@ -813,7 +826,8 @@ def full_flow_cc_solver( turb_Cts[:, :, i:i+1], TSR_i, axial_induction_i, - scale=2.0 + flow_field.wind_shear, + scale=2.0, ) # NOTE: exponential @@ -830,7 +844,7 @@ def full_flow_cc_solver( turbine_grid_farm.rotor_diameters_sorted[:, :, :, None, None], turb_u_wake, Ctmp, - **deficit_model_args + **deficit_model_args, ) flow_field.v_sorted += v_wake @@ -928,9 +942,9 @@ def turbopark_solver( axial_induction_i = axial_induction_i[:, :, 0:1, None, None] turbulence_intensity_i = turbine_turbulence_intensity[:, :, i:i+1] yaw_angle_i = farm.yaw_angles_sorted[:, :, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[: ,:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[: ,:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[: ,:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, :, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, :, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, :, i:i+1, None, None] effective_yaw_i = np.zeros_like(yaw_angle_i) effective_yaw_i += yaw_angle_i @@ -946,7 +960,8 @@ def turbopark_solver( hub_height_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) effective_yaw_i += added_yaw @@ -980,7 +995,7 @@ def turbopark_solver( cubature_weights=grid.cubature_weights ) ct_ii = ct_ii[:, :, 0:1, None, None] - rotor_diameter_ii = farm.rotor_diameters_sorted[: ,:, ii:ii+1, None, None] + rotor_diameter_ii = farm.rotor_diameters_sorted[:, :, ii:ii+1, None, None] deflection_field_ii = model_manager.deflection_model.function( x_ii, @@ -989,10 +1004,10 @@ def turbopark_solver( turbulence_intensity_ii, ct_ii, rotor_diameter_ii, - **deflection_model_args + **deflection_model_args, ) - deflection_field[:,:,ii:ii+1,:,:] = deflection_field_ii[:,:,i:i+1,:,:] + deflection_field[:, :, ii:ii+1, :, :] = deflection_field_ii[:, :, i:i+1, :, :] if model_manager.enable_transverse_velocities: v_wake, w_wake = calculate_transverse_velocity( @@ -1007,7 +1022,8 @@ def turbopark_solver( yaw_angle_i, ct_i, TSR_i, - axial_induction_i + axial_induction_i, + flow_field.wind_shear, ) if model_manager.enable_yaw_added_recovery: @@ -1033,7 +1049,7 @@ def turbopark_solver( farm.rotor_diameters_sorted[:, :, :, None, None], i, deflection_field, - **deficit_model_args + **deficit_model_args, ) wake_field = model_manager.combination_model.function( @@ -1064,9 +1080,9 @@ def turbopark_solver( ti_added = ( area_overlap * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * np.array(grid.x_sorted > x_i) - * np.array(np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * np.array(grid.x_sorted <= downstream_influence_length + x_i) + * (grid.x_sorted > x_i) + * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) + * (grid.x_sorted <= downstream_influence_length + x_i) ) # Combine turbine TIs with WAT @@ -1111,7 +1127,6 @@ def full_flow_turbopark_solver( # turbine_grid_farm.construc_turbine_pPs() # turbine_grid_farm.construct_coordinates() - # turbine_grid = TurbineGrid( # turbine_coordinates=turbine_grid_farm.coordinates, # reference_turbine_diameter=turbine_grid_farm.rotor_diameters, @@ -1494,3 +1509,208 @@ def full_flow_empirical_gauss_solver( flow_field.u_sorted = flow_field.u_initial_sorted - wake_field flow_field.v_sorted += v_wake flow_field.w_sorted += w_wake + + +def sequential_multidim_solver( + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + model_manager: WakeModelManager +) -> None: + # Algorithm + # For each turbine, calculate its effect on every downstream turbine. + # For the current turbine, we are calculating the deficit that it adds to downstream turbines. + # Integrate this into the main data structure. + # Move on to the next turbine. + + # <> + deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) + deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) + downselect_turbine_fCts = multidim_Ct_down_select( + farm.turbine_fCts_sorted, + flow_field.multidim_conditions, + ) + + # This is u_wake + wake_field = np.zeros_like(flow_field.u_initial_sorted) + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + + turbine_turbulence_intensity = ( + flow_field.turbulence_intensity + * np.ones((flow_field.n_wind_directions, flow_field.n_wind_speeds, farm.n_turbines, 1, 1)) + ) + ambient_turbulence_intensity = flow_field.turbulence_intensity + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + x_i = np.mean(grid.x_sorted[:, :, i:i+1], axis=(3, 4)) + x_i = x_i[:, :, :, None, None] + y_i = np.mean(grid.y_sorted[:, :, i:i+1], axis=(3, 4)) + y_i = y_i[:, :, :, None, None] + z_i = np.mean(grid.z_sorted[:, :, i:i+1], axis=(3, 4)) + z_i = z_i[:, :, :, None, None] + + u_i = flow_field.u_sorted[:, :, i:i+1] + v_i = flow_field.v_sorted[:, :, i:i+1] + + ct_i = Ct_multidim( + velocities=flow_field.u_sorted, + yaw_angle=farm.yaw_angles_sorted, + tilt_angle=farm.tilt_angles_sorted, + ref_tilt_cp_ct=farm.ref_tilt_cp_cts_sorted, + fCt=downselect_turbine_fCts, + tilt_interp=farm.turbine_fTilts, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights + ) + # Since we are filtering for the i'th turbine in the Ct function, + # get the first index here (0:1) + ct_i = ct_i[:, :, 0:1, None, None] + axial_induction_i = axial_induction_multidim( + velocities=flow_field.u_sorted, + yaw_angle=farm.yaw_angles_sorted, + tilt_angle=farm.tilt_angles_sorted, + ref_tilt_cp_ct=farm.ref_tilt_cp_cts_sorted, + fCt=downselect_turbine_fCts, + tilt_interp=farm.turbine_fTilts, + correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights + ) + # Since we are filtering for the i'th turbine in the axial induction function, + # get the first index here (0:1) + axial_induction_i = axial_induction_i[:, :, 0:1, None, None] + turbulence_intensity_i = turbine_turbulence_intensity[:, :, i:i+1] + yaw_angle_i = farm.yaw_angles_sorted[:, :, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, :, i:i+1, None, None] + rotor_diameter_i = farm.rotor_diameters_sorted[:, :, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, :, i:i+1, None, None] + + effective_yaw_i = np.zeros_like(yaw_angle_i) + effective_yaw_i += yaw_angle_i + + if model_manager.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + flow_field.u_initial_sorted, + grid.y_sorted[:, :, i:i+1] - y_i, + grid.z_sorted[:, :, i:i+1], + rotor_diameter_i, + hub_height_i, + ct_i, + TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + effective_yaw_i += added_yaw + + # Model calculations + # NOTE: exponential + deflection_field = model_manager.deflection_model.function( + x_i, + y_i, + effective_yaw_i, + turbulence_intensity_i, + ct_i, + rotor_diameter_i, + **deflection_model_args, + ) + + if model_manager.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - x_i, + grid.y_sorted - y_i, + grid.z_sorted, + rotor_diameter_i, + hub_height_i, + yaw_angle_i, + ct_i, + TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + + if model_manager.enable_yaw_added_recovery: + I_mixing = yaw_added_turbulence_mixing( + u_i, + turbulence_intensity_i, + v_i, + flow_field.w_sorted[:, :, i:i+1], + v_wake[:, :, i:i+1], + w_wake[:, :, i:i+1], + ) + gch_gain = 2 + turbine_turbulence_intensity[:, :, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing + + # NOTE: exponential + velocity_deficit = model_manager.velocity_model.function( + x_i, + y_i, + z_i, + axial_induction_i, + deflection_field, + yaw_angle_i, + turbulence_intensity_i, + ct_i, + hub_height_i, + rotor_diameter_i, + **deficit_model_args, + ) + + wake_field = model_manager.combination_model.function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + wake_added_turbulence_intensity = model_manager.turbulence_model.function( + ambient_turbulence_intensity, + grid.x_sorted, + x_i, + rotor_diameter_i, + axial_induction_i, + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(3, 4)) + / (grid.grid_resolution * grid.grid_resolution) + ) + area_overlap = area_overlap[:, :, :, None, None] + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (grid.x_sorted > x_i) + * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) + * (grid.x_sorted <= downstream_influence_length + x_i) + ) + + # Combine turbine TIs with WAT + turbine_turbulence_intensity = np.maximum( + np.sqrt( ti_added ** 2 + ambient_turbulence_intensity ** 2 ), + turbine_turbulence_intensity + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(3,4) + )[:, :, :, None, None] diff --git a/floris/simulation/turbine.py b/floris/simulation/turbine.py index cdf457878..0c056322d 100644 --- a/floris/simulation/turbine.py +++ b/floris/simulation/turbine.py @@ -142,7 +142,7 @@ def compute_tilt_angles_for_floating_turbines( else: tilt_angles += ( tilt_interp[turb_type](rotor_effective_velocities) - * np.array(turbine_type_map == turb_type) + * (turbine_type_map == turb_type) ) # TODO: Not sure if this is the best way to do this? Basically replaces the initialized @@ -267,7 +267,7 @@ def power( # type to the main thrust coefficient array p += ( power_interp[turb_type](rotor_effective_velocities) - * np.array(turbine_type_map == turb_type) + * (turbine_type_map == turb_type) ) return p * ref_density_cp_ct @@ -355,7 +355,7 @@ def Ct( # type to the main thrust coefficient array thrust_coefficient += ( fCt[turb_type](average_velocities) - * np.array(turbine_type_map == turb_type) + * (turbine_type_map == turb_type) ) thrust_coefficient = np.clip(thrust_coefficient, 0.0001, 0.9999) effective_thrust = thrust_coefficient * cosd(yaw_angle) * cosd(tilt_angle - ref_tilt_cp_ct) @@ -521,9 +521,9 @@ class PowerThrustTable(FromDictMixin): ValueError: Raised if the power, thrust, and wind_speed are not all 1-d array-like shapes. ValueError: Raised if power, thrust, and wind_speed don't have the same number of values. """ - power: NDArrayFloat = field(converter=floris_array_converter) - thrust: NDArrayFloat = field(converter=floris_array_converter) - wind_speed: NDArrayFloat = field(converter=floris_array_converter) + power: NDArrayFloat = field(default=[], converter=floris_array_converter) + thrust: NDArrayFloat = field(default=[], converter=floris_array_converter) + wind_speed: NDArrayFloat = field(default=[], converter=floris_array_converter) def __attrs_post_init__(self) -> None: # Validate the power, thrust, and wind speed inputs. @@ -624,9 +624,11 @@ class Turbine(BaseClass): generator_efficiency: float = field() ref_density_cp_ct: float = field() ref_tilt_cp_ct: float = field() - power_thrust_table: PowerThrustTable = field(converter=PowerThrustTable.from_dict) - floating_tilt_table = field(default=None) - floating_correct_cp_ct_for_tilt = field(default=None) + power_thrust_table: PowerThrustTable = field(default=None) + floating_tilt_table: TiltTable = field(default=None) + floating_correct_cp_ct_for_tilt: bool = field(default=None) + power_thrust_data_file: str = field(default=None) + multi_dimensional_cp_ct: bool = field(default=False) # rloc: float = float_attrib() # TODO: goes here or on the Grid? # use_points_on_perimeter: bool = bool_attrib() @@ -650,6 +652,7 @@ class Turbine(BaseClass): def __attrs_post_init__(self) -> None: # Post-init initialization for the power curve interpolation functions + self.power_thrust_table = PowerThrustTable.from_dict(self.power_thrust_table) wind_speeds = self.power_thrust_table.wind_speed self.fCp_interp = interp1d( wind_speeds, @@ -665,7 +668,9 @@ def __attrs_post_init__(self) -> None: ) self.power_interp = interp1d( wind_speeds, - inner_power + inner_power, + bounds_error=False, + fill_value=0 ) """ diff --git a/floris/simulation/turbine_multi_dim.py b/floris/simulation/turbine_multi_dim.py new file mode 100644 index 000000000..3e2cc7b8d --- /dev/null +++ b/floris/simulation/turbine_multi_dim.py @@ -0,0 +1,518 @@ +# Copyright 2023 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +from __future__ import annotations + +import copy +from collections.abc import Iterable + +import numpy as np +import pandas as pd +from attrs import define, field +from flatten_dict import flatten +from scipy.interpolate import interp1d + +# import floris.simulation.turbine as turbine +from floris.simulation import ( + average_velocity, + compute_tilt_angles_for_floating_turbines, + TiltTable, + Turbine, +) +from floris.simulation.turbine import _filter_convert +from floris.type_dec import ( + NDArrayBool, + NDArrayFilter, + NDArrayFloat, + NDArrayInt, + NDArrayObject, +) +from floris.utilities import cosd + + +def power_multidim( + ref_density_cp_ct: float, + rotor_effective_velocities: NDArrayFloat, + power_interp: NDArrayObject, + ix_filter: NDArrayInt | Iterable[int] | None = None, +) -> NDArrayFloat: + """Power produced by a turbine defined with multi-dimensional + Cp/Ct values, adjusted for yaw and tilt. Value given in Watts. + + Args: + ref_density_cp_cts (NDArrayFloat[wd, ws, turbines]): The reference density for each turbine + rotor_effective_velocities (NDArrayFloat[wd, ws, turbines, grid1, grid2]): The rotor + effective velocities at a turbine. + power_interp (NDArrayObject[wd, ws, turbines]): The power interpolation function + for each turbine. + ix_filter (NDArrayInt, optional): The boolean array, or + integer indices to filter out before calculation. Defaults to None. + + Returns: + NDArrayFloat: The power, in Watts, for each turbine after adjusting for yaw and tilt. + """ + # TODO: Change the order of input arguments to be consistent with the other + # utility functions - velocities first... + # Update to power calculation which replaces the fixed pP exponent with + # an exponent pW, that changes the effective wind speed input to the power + # calculation, rather than scaling the power. This better handles power + # loss to yaw in above rated conditions + # + # based on the paper "Optimising yaw control at wind farm level" by + # Ervin Bossanyi + + # TODO: check this - where is it? + # P = 1/2 rho A V^3 Cp + + # Down-select inputs if ix_filter is given + if ix_filter is not None: + ix_filter = _filter_convert(ix_filter, rotor_effective_velocities) + power_interp = power_interp[:, :, ix_filter] + rotor_effective_velocities = rotor_effective_velocities[:, :, ix_filter] + # Loop over each turbine to get power for all turbines + p = np.zeros(np.shape(rotor_effective_velocities)) + for i, wd in enumerate(power_interp): + for j, ws in enumerate(wd): + for k, turb in enumerate(ws): + p[i, j, k] = power_interp[i, j, k](rotor_effective_velocities[i, j, k]) + + return p * ref_density_cp_ct + + +def Ct_multidim( + velocities: NDArrayFloat, + yaw_angle: NDArrayFloat, + tilt_angle: NDArrayFloat, + ref_tilt_cp_ct: NDArrayFloat, + fCt: list, + tilt_interp: NDArrayObject, + correct_cp_ct_for_tilt: NDArrayBool, + turbine_type_map: NDArrayObject, + ix_filter: NDArrayFilter | Iterable[int] | None = None, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None +) -> NDArrayFloat: + + """Thrust coefficient of a turbine defined with multi-dimensional + Cp/Ct values, incorporating the yaw angle. The value is interpolated + from the coefficient of thrust vs wind speed table using the rotor + swept area average velocity. + + Args: + velocities (NDArrayFloat[wd, ws, turbines, grid1, grid2]): The velocity field at + a turbine. + yaw_angle (NDArrayFloat[wd, ws, turbines]): The yaw angle for each turbine. + tilt_angle (NDArrayFloat[wd, ws, turbines]): The tilt angle for each turbine. + ref_tilt_cp_ct (NDArrayFloat[wd, ws, turbines]): The reference tilt angle for each turbine + that the Cp/Ct tables are defined at. + fCt (list): The thrust coefficient interpolation functions for each turbine. + tilt_interp (Iterable[tuple]): The tilt interpolation functions for each + turbine. + correct_cp_ct_for_tilt (NDArrayBool[wd, ws, turbines]): Boolean for determining if the + turbines Cp and Ct should be corrected for tilt. + turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition + for each turbine. + ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or + integer indices as an iterable of array to filter out before calculation. + Defaults to None. + + Returns: + NDArrayFloat: Coefficient of thrust for each requested turbine. + """ + + if isinstance(yaw_angle, list): + yaw_angle = np.array(yaw_angle) + + if isinstance(tilt_angle, list): + tilt_angle = np.array(tilt_angle) + + # Down-select inputs if ix_filter is given + if ix_filter is not None: + ix_filter = _filter_convert(ix_filter, yaw_angle) + velocities = velocities[:, :, ix_filter] + yaw_angle = yaw_angle[:, :, ix_filter] + tilt_angle = tilt_angle[:, :, ix_filter] + ref_tilt_cp_ct = ref_tilt_cp_ct[:, :, ix_filter] + fCt = fCt[:, :, ix_filter] + turbine_type_map = turbine_type_map[:, :, ix_filter] + correct_cp_ct_for_tilt = correct_cp_ct_for_tilt[:, :, ix_filter] + + average_velocities = average_velocity( + velocities, + method=average_method, + cubature_weights=cubature_weights + ) + + # Compute the tilt, if using floating turbines + old_tilt_angle = copy.deepcopy(tilt_angle) + tilt_angle = compute_tilt_angles_for_floating_turbines( + turbine_type_map, + tilt_angle, + tilt_interp, + average_velocities, + ) + # Only update tilt angle if requested (if the tilt isn't accounted for in the Ct curve) + tilt_angle = np.where(correct_cp_ct_for_tilt, tilt_angle, old_tilt_angle) + + # Loop over each turbine to get thrust coefficient for all turbines + thrust_coefficient = np.zeros(np.shape(average_velocities)) + for i, wd in enumerate(fCt): + for j, ws in enumerate(wd): + for k, turb in enumerate(ws): + thrust_coefficient[i, j, k] = fCt[i, j, k](average_velocities[i, j, k]) + thrust_coefficient = np.clip(thrust_coefficient, 0.0001, 0.9999) + effective_thrust = thrust_coefficient * cosd(yaw_angle) * cosd(tilt_angle - ref_tilt_cp_ct) + return effective_thrust + + +def axial_induction_multidim( + velocities: NDArrayFloat, # (wind directions, wind speeds, turbines, grid, grid) + yaw_angle: NDArrayFloat, # (wind directions, wind speeds, turbines) + tilt_angle: NDArrayFloat, # (wind directions, wind speeds, turbines) + ref_tilt_cp_ct: NDArrayFloat, + fCt: list, # (turbines) + tilt_interp: NDArrayObject, # (turbines) + correct_cp_ct_for_tilt: NDArrayBool, # (wind directions, wind speeds, turbines) + turbine_type_map: NDArrayObject, # (wind directions, 1, turbines) + ix_filter: NDArrayFilter | Iterable[int] | None = None, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None +) -> NDArrayFloat: + """Axial induction factor of the turbines defined with multi-dimensional + Cp/Ct values, incorporating the thrust coefficient and yaw angle. + + Args: + velocities (NDArrayFloat): The velocity field at each turbine; should be shape: + (number of turbines, ngrid, ngrid), or (ngrid, ngrid) for a single turbine. + yaw_angle (NDArrayFloat[wd, ws, turbines]): The yaw angle for each turbine. + tilt_angle (NDArrayFloat[wd, ws, turbines]): The tilt angle for each turbine. + ref_tilt_cp_ct (NDArrayFloat[wd, ws, turbines]): The reference tilt angle for each turbine + that the Cp/Ct tables are defined at. + fCt (list): The thrust coefficient interpolation functions for each turbine. + tilt_interp (Iterable[tuple]): The tilt interpolation functions for each + turbine. + correct_cp_ct_for_tilt (NDArrayBool[wd, ws, turbines]): Boolean for determining if the + turbines Cp and Ct should be corrected for tilt. + turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition + for each turbine. + ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or + integer indices (as an aray or iterable) to filter out before calculation. + Defaults to None. + + Returns: + Union[float, NDArrayFloat]: [description] + """ + + if isinstance(yaw_angle, list): + yaw_angle = np.array(yaw_angle) + + # TODO: Should the tilt_angle used for the return calculation be modified the same as the + # tilt_angle in Ct, if the user has supplied a tilt/wind_speed table? + if isinstance(tilt_angle, list): + tilt_angle = np.array(tilt_angle) + + # Get Ct first before modifying any data + thrust_coefficient = Ct_multidim( + velocities, + yaw_angle, + tilt_angle, + ref_tilt_cp_ct, + fCt, + tilt_interp, + correct_cp_ct_for_tilt, + turbine_type_map, + ix_filter, + average_method, + cubature_weights + ) + + # Then, process the input arguments as needed for this function + ix_filter = _filter_convert(ix_filter, yaw_angle) + if ix_filter is not None: + yaw_angle = yaw_angle[:, :, ix_filter] + tilt_angle = tilt_angle[:, :, ix_filter] + ref_tilt_cp_ct = ref_tilt_cp_ct[:, :, ix_filter] + + return ( + 0.5 + / (cosd(yaw_angle) + * cosd(tilt_angle - ref_tilt_cp_ct)) + * ( + 1 - np.sqrt( + 1 - thrust_coefficient * cosd(yaw_angle) * cosd(tilt_angle - ref_tilt_cp_ct) + ) + ) + ) + + +def multidim_Ct_down_select( + turbine_fCts, + conditions, +) -> list: + """ + Ct interpolants are down selected from the multi-dimensional Ct data + provided for the turbine based on the specified conditions. + + Args: + turbine_fCts (NDArray[wd, ws, turbines]): The Ct interpolants generated from the + multi-dimensional Ct turbine data for all specified conditions. + conditions (dict): The conditions at which to determine which Ct interpolant to use. + + Returns: + NDArray: The downselected Ct interpolants for the selected conditions. + """ + downselect_turbine_fCts = np.empty_like(turbine_fCts) + # Loop over the wind directions, wind speeds, and turbines, finding the Ct interpolant + # that is closest to the specified multi-dimensional condition. + for i, wd in enumerate(turbine_fCts): + for j, ws in enumerate(wd): + for k, turb in enumerate(ws): + # Get the interpolant keys in float type for comparison + keys_float = np.array([[float(v) for v in val] for val in turb.keys()]) + + # Find the nearest key to the specified conditions. + key_vals = [] + for ii, cond in enumerate(conditions.values()): + key_vals.append( + keys_float[:, ii][np.absolute(keys_float[:, ii] - cond).argmin()] + ) + + downselect_turbine_fCts[i, j, k] = turb[tuple(key_vals)] + + return downselect_turbine_fCts + + +def multidim_power_down_select( + power_interps, + conditions, +) -> list: + """ + Cp interpolants are down selected from the multi-dimensional Cp data + provided for the turbine based on the specified conditions. + + Args: + power_interps (NDArray[wd, ws, turbines]): The power interpolants generated from the + multi-dimensional Cp turbine data for all specified conditions. + conditions (dict): The conditions at which to determine which Ct interpolant to use. + + Returns: + NDArray: The downselected power interpolants for the selected conditions. + """ + downselect_power_interps = np.empty_like(power_interps) + # Loop over the wind directions, wind speeds, and turbines, finding the power interpolant + # that is closest to the specified multi-dimensional condition. + for i, wd in enumerate(power_interps): + for j, ws in enumerate(wd): + for k, turb in enumerate(ws): + # Get the interpolant keys in float type for comparison + keys_float = np.array([[float(v) for v in val] for val in turb.keys()]) + + # Find the nearest key to the specified conditions. + key_vals = [] + for ii, cond in enumerate(conditions.values()): + key_vals.append( + keys_float[:, ii][np.absolute(keys_float[:, ii] - cond).argmin()] + ) + + # Use the constructed key to choose the correct interpolant + downselect_power_interps[i, j, k] = turb[tuple(key_vals)] + + return downselect_power_interps + + +@define +class MultiDimensionalPowerThrustTable(): + """Helper class to convert the multi-dimensional inputs to a dictionary of objects. + """ + + @classmethod + def from_dataframe(self, df) -> None: + # Validate the dataframe + if not all(ele in df.columns.values.tolist() for ele in ["ws", "Cp", "Ct"]): + print(df.columns.values.tolist()) + raise ValueError("Multidimensional data missing required ws/Cp/Ct data.") + if df.columns.values[-3:].tolist() != ["ws", "Cp", "Ct"]: + print(df.columns.values[-3:].tolist()) + raise ValueError( + "Multidimensional data not in correct form. ws, Cp, and Ct must be " + "defined as the last 3 columns, in that order." + ) + + # Extract the supplied dimensions, minus the required ws, Cp, and Ct columns. + keys = df.columns.values[:-3].tolist() + values = [df[df.columns.values[i]].unique().tolist() for i in range(len(keys))] + values = [[str(val) for val in value] for value in values] + + # Functions for recursively building a nested dictionary from + # an arbitrary number of paired-inputs. + def add_level(obj, k, v): + tmp = {} + for val in v: + tmp.update({val: []}) + obj.update({k: tmp}) + return obj + + def add_sub_level(obj, k): + tmp = {} + for key in k: + tmp.update({key: obj}) + return tmp + + obj = {} + # Reverse the lists to start from the lowest level of the dictionary + keys.reverse() + values.reverse() + # Recursively build a nested dictionary from the user-supplied dimensions + for i, key in enumerate(keys): + if i == 0: + obj = add_level(obj, key, values[i]) + else: + obj = add_sub_level(obj, values[i]) + obj = {key: obj} + + return flatten(obj) + + +@define +class TurbineMultiDimensional(Turbine): + """ + Turbine is a class containing objects pertaining to the individual + turbines. + + Turbine is a model class representing a particular wind turbine. It + is largely a container of data and parameters, but also contains + methods to probe properties for output. + + Parameters: + rotor_diameter (:py:obj: float): The rotor diameter (m). + hub_height (:py:obj: float): The hub height (m). + pP (:py:obj: float): The cosine exponent relating the yaw + misalignment angle to power. + pT (:py:obj: float): The cosine exponent relating the rotor + tilt angle to power. + generator_efficiency (:py:obj: float): The generator + efficiency factor used to scale the power production. + ref_density_cp_ct (:py:obj: float): The density at which the provided + cp and ct is defined + power_thrust_table (PowerThrustTable): A dictionary containing the + following key-value pairs: + + power (:py:obj: List[float]): The coefficient of power at + different wind speeds. + thrust (:py:obj: List[float]): The coefficient of thrust + at different wind speeds. + wind_speed (:py:obj: List[float]): The wind speeds for + which the power and thrust values are provided (m/s). + ngrid (*int*, optional): The square root of the number + of points to use on the turbine grid. This number will be + squared so that the points can be evenly distributed. + Defaults to 5. + rloc (:py:obj: float, optional): A value, from 0 to 1, that determines + the width/height of the grid of points on the rotor as a ratio of + the rotor radius. + Defaults to 0.5. + """ + + power_thrust_data_file: str = field(default=None) + multi_dimensional_cp_ct: bool = field(default=False) + + # rloc: float = float_attrib() # TODO: goes here or on the Grid? + # use_points_on_perimeter: bool = bool_attrib() + + # Initialized in the post_init function + # rotor_radius: float = field(init=False) + # rotor_area: float = field(init=False) + # fCp_interp: interp1d = field(init=False) + # fCt_interp: interp1d = field(init=False) + # power_interp: interp1d = field(init=False) + # tilt_interp: interp1d = field(init=False) + + + # For the following parameters, use default values if not user-specified + # self.rloc = float(input_dictionary["rloc"]) if "rloc" in input_dictionary else 0.5 + # if "use_points_on_perimeter" in input_dictionary: + # self.use_points_on_perimeter = bool(input_dictionary["use_points_on_perimeter"]) + # else: + # self.use_points_on_perimeter = False + + def __attrs_post_init__(self) -> None: + + # Read in the multi-dimensional data supplied by the user. + df = pd.read_csv(self.power_thrust_data_file) + + # Build the multi-dimensional power/thrust table + self.power_thrust_data = MultiDimensionalPowerThrustTable.from_dataframe(df) + + # Create placeholders for the interpolation functions + self.fCt_interp = {} + self.power_interp = {} + + # Down-select the DataFrame to have just the ws, Cp, and Ct values + index_col = df.columns.values[:-3] + df2 = df.set_index(index_col.tolist()) + + # Loop over the multi-dimensional keys to get the correct ws/Cp/Ct data to make + # the Ct and power interpolants. + for key in df2.index.unique(): + # Select the correct ws/Cp/Ct data + data = df2.loc[key] + + # Build the interpolants + wind_speeds = data['ws'].values + self.fCp_interp = interp1d( + wind_speeds, + data['Cp'].values, + fill_value=(0.0, 1.0), + bounds_error=False, + ) + inner_power = ( + 0.5 * self.rotor_area + * self.fCp_interp(wind_speeds) + * self.generator_efficiency + * wind_speeds ** 3 + ) + self.power_interp.update({ + key: interp1d( + wind_speeds, + inner_power, + bounds_error=False, + fill_value=0 + ) + }) + self.fCt_interp.update({ + key: interp1d( + wind_speeds, + data['Ct'].values, + fill_value=(0.0001, 0.9999), + bounds_error=False, + ) + }) + + # If defined, create a tilt interpolation function for floating turbines. + # fill_value currently set to apply the min or max tilt angles if outside + # of the interpolation range. + if self.floating_tilt_table is not None: + self.floating_tilt_table = TiltTable.from_dict(self.floating_tilt_table) + self.fTilt_interp = interp1d( + self.floating_tilt_table.wind_speeds, + self.floating_tilt_table.tilt, + fill_value=(0.0, self.floating_tilt_table.tilt[-1]), + bounds_error=False, + ) + self.tilt_interp = self.fTilt_interp + self.correct_cp_ct_for_tilt = self.floating_correct_cp_ct_for_tilt + else: + self.fTilt_interp = None + self.tilt_interp = None + self.correct_cp_ct_for_tilt = False diff --git a/floris/simulation/wake.py b/floris/simulation/wake.py index a141c94be..558f6ecbe 100644 --- a/floris/simulation/wake.py +++ b/floris/simulation/wake.py @@ -65,7 +65,8 @@ "gauss": GaussVelocityDeficit, "jensen": JensenVelocityDeficit, "turbopark": TurbOParkVelocityDeficit, - "empirical_gauss": EmpiricalGaussVelocityDeficit + "empirical_gauss": EmpiricalGaussVelocityDeficit, + "multidim_cp_ct": GaussVelocityDeficit }, } diff --git a/floris/simulation/wake_deflection/empirical_gauss.py b/floris/simulation/wake_deflection/empirical_gauss.py index d4019efd6..864eafed8 100644 --- a/floris/simulation/wake_deflection/empirical_gauss.py +++ b/floris/simulation/wake_deflection/empirical_gauss.py @@ -60,7 +60,7 @@ class EmpiricalGaussVelocityDeflection(BaseModel): """ horizontal_deflection_gain_D: float = field(default=3.0) vertical_deflection_gain_D: float = field(default=-1) - deflection_rate: float = field(default=15) + deflection_rate: float = field(default=30) mixing_gain_deflection: float = field(default=0.0) yaw_added_mixing_gain: float = field(default=0.0) @@ -126,7 +126,7 @@ def function( A_z = (deflection_gain_z * ct_i * tilt_r) / (1 + self.mixing_gain_deflection * mixing_i) # Apply downstream mask in the process - x_normalized = (x - x_i) * np.array(x > x_i + 0.1) / rotor_diameter_i + x_normalized = (x - x_i) * (x > x_i + 0.1) / rotor_diameter_i log_term = np.log( (x_normalized - self.deflection_rate) / (x_normalized + self.deflection_rate) diff --git a/floris/simulation/wake_deflection/gauss.py b/floris/simulation/wake_deflection/gauss.py index e9f6ae0b8..25caff58e 100644 --- a/floris/simulation/wake_deflection/gauss.py +++ b/floris/simulation/wake_deflection/gauss.py @@ -10,10 +10,14 @@ # License for the specific language governing permissions and limitations under # the License. -from typing import Any, Dict +from __future__ import annotations +from typing import Any + +import numexpr as ne import numpy as np from attrs import define, field +from numpy import pi from floris.simulation import ( BaseModel, @@ -72,6 +76,7 @@ class GaussVelocityDeflection(BaseModel): :filter: docname in docnames :keyprefix: gdm- """ + ad: float = field(converter=float, default=0.0) bd: float = field(converter=float, default=0.0) alpha: float = field(converter=float, default=0.58) @@ -86,7 +91,7 @@ def prepare_function( self, grid: Grid, flow_field: FlowField, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: kwargs = { "x": grid.x_sorted, @@ -141,18 +146,18 @@ def function( # ============================================================== # Opposite sign convention in this model - yaw_i = -1 * yaw_i + yaw_i *= -1 # TODO: connect support for tilt - tilt = 0.0 #turbine.tilt_angle + tilt = 0.0 # turbine.tilt_angle # initial velocity deficits uR = ( freestream_velocity - * ct_i - * cosd(tilt) - * cosd(yaw_i) - / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) + * ct_i + * cosd(tilt) + * cosd(yaw_i) + / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) ) u0 = freestream_velocity * np.sqrt(1 - ct_i) @@ -171,10 +176,10 @@ def function( C0 = 1 - u0 / freestream_velocity M0 = C0 * (2 - C0) - E0 = C0 ** 2 - 3 * np.exp(1.0 / 12.0) * C0 + 3 * np.exp(1.0 / 3.0) + E0 = ne.evaluate("C0 ** 2 - 3 * exp(1.0 / 12.0) * C0 + 3 * exp(1.0 / 3.0)") # initial Gaussian wake expansion - sigma_z0 = rotor_diameter_i * 0.5 * np.sqrt(uR / (freestream_velocity + u0)) + sigma_z0 = ne.evaluate("rotor_diameter_i * 0.5 * sqrt(uR / (freestream_velocity + u0))") sigma_y0 = sigma_z0 * cosd(yaw_i) * cosd(wind_veer) # yR = y - y_i @@ -189,31 +194,29 @@ def function( # deflection in the near wake delta_near_wake = ((x - xR) / (x0 - xR)) * delta0 + (self.ad + self.bd * (x - x_i)) - delta_near_wake = delta_near_wake * np.array(x >= xR) - delta_near_wake = delta_near_wake * np.array(x <= x0) + delta_near_wake *= (x >= xR) & (x <= x0) # deflection in the far wake sigma_y = ky * (x - x0) + sigma_y0 sigma_z = kz * (x - x0) + sigma_z0 - sigma_y = sigma_y * np.array(x >= x0) + sigma_y0 * np.array(x < x0) - sigma_z = sigma_z * np.array(x >= x0) + sigma_z0 * np.array(x < x0) - - ln_deltaNum = (1.6 + np.sqrt(M0)) * ( - 1.6 * np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) - np.sqrt(M0) - ) - ln_deltaDen = (1.6 - np.sqrt(M0)) * ( - 1.6 * np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) + np.sqrt(M0) - ) - - delta_far_wake = ( - delta0 - + theta_c0 * E0 / 5.2 - * np.sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0)) - * np.log(ln_deltaNum / ln_deltaDen) - + (self.ad + self.bd * (x - x_i)) + sigma_y = sigma_y * (x >= x0) + sigma_y0 * (x < x0) + sigma_z = sigma_z * (x >= x0) + sigma_z0 * (x < x0) + + M0_sqrt = np.sqrt(M0) + middle_term = np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) + ln_deltaNum = (1.6 + M0_sqrt) * (1.6 * middle_term - M0_sqrt) + ln_deltaDen = (1.6 - M0_sqrt) * (1.6 * middle_term + M0_sqrt) + + middle_term = ne.evaluate( + "theta_c0" + " * E0" + " / 5.2" + " * sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0))" + " * log(ln_deltaNum / ln_deltaDen)" ) + delta_far_wake = delta0 + middle_term + (self.ad + self.bd * (x - x_i)) - delta_far_wake = delta_far_wake * np.array(x > x0) + delta_far_wake = delta_far_wake * (x > x0) deflection = delta_near_wake + delta_far_wake return deflection @@ -240,10 +243,9 @@ def gamma( [type]: [description] """ # NOTE the cos commented below is included in Ct - return scale * (np.pi / 8) * D * velocity * Uinf * Ct # * cosd(yaw) + return scale * (pi / 8) * D * velocity * Uinf * Ct # * cosd(yaw) -# def calculate_effective_yaw( def wake_added_yaw( u_i, v_i, @@ -255,6 +257,7 @@ def wake_added_yaw( ct_i, tip_speed_ratio, axial_induction_i, + wind_shear, scale=1.0, ): """ @@ -272,17 +275,17 @@ def wake_added_yaw( Ct = ct_i # (wd, ws, 1, 1, 1) for the current turbine TSR = tip_speed_ratio # scalar aI = axial_induction_i # (wd, ws, 1, 1, 1) for the current turbine - avg_v = np.mean(v_i, axis=(3,4)) # (wd, ws, 1, grid, grid) + avg_v = np.mean(v_i, axis=(3, 4)) # (wd, ws, 1, grid, grid) # flow parameters - Uinf = np.mean(u_initial, axis=(2,3,4)) - Uinf = Uinf[:,:,None,None,None] + Uinf = np.mean(u_initial, axis=(2, 3, 4)) + Uinf = Uinf[:, :, None, None, None] # TODO: Allow user input for eps gain eps_gain = 0.2 eps = eps_gain * D # Use set value - vel_top = ((HH + D / 2) / HH) ** 0.12 * np.ones((1, 1, 1, 1, 1)) + vel_top = ((HH + D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1, 1)) Gamma_top = gamma( D, vel_top, @@ -291,7 +294,7 @@ def wake_added_yaw( scale, ) - vel_bottom = ((HH - D / 2) / HH) ** 0.12 * np.ones((1, 1, 1, 1, 1)) + vel_bottom = ((HH - D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1, 1)) Gamma_bottom = -1 * gamma( D, vel_bottom, @@ -300,9 +303,8 @@ def wake_added_yaw( scale, ) - turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(3,4))) - turbine_average_velocity = turbine_average_velocity[:,:,:,None,None] - Gamma_wake_rotation = 0.25 * 2 * np.pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR + turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(3, 4)))[:, :, :, None, None] + Gamma_wake_rotation = 0.25 * 2 * pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR ### compute the spanwise and vertical velocities induced by yaw @@ -312,37 +314,37 @@ def wake_added_yaw( # top vortex # NOTE: this is the top of the grid, not the top of the rotor zT = z_i - (HH + D / 2) + BaseModel.NUM_EPS # distance from the top of the grid - rT = yLocs ** 2 + zT ** 2 # TODO: This is - in the paper + rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is (-) in the paper # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = 1 - np.exp(-rT / (eps ** 2)) - v_top = (Gamma_top * zT) / (2 * np.pi * rT) * core_shape + core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") + v_top = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape") v_top = np.mean( v_top, axis=(3,4) ) - # w_top = (-1 * Gamma_top * yLocs) / (2 * np.pi * rT) * core_shape * decay + # w_top = (-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay # bottom vortex zB = z_i - (HH - D / 2) + BaseModel.NUM_EPS - rB = yLocs ** 2 + zB ** 2 - core_shape = 1 - np.exp(-rB / (eps ** 2)) - v_bottom = (Gamma_bottom * zB) / (2 * np.pi * rB) * core_shape + rB = ne.evaluate("yLocs ** 2 + zB ** 2") + core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") + v_bottom = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape") v_bottom = np.mean( v_bottom, axis=(3,4) ) - # w_bottom = (-1 * Gamma_bottom * yLocs) / (2 * np.pi * rB) * core_shape * decay + # w_bottom = (-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay # wake rotation vortex zC = z_i - HH + BaseModel.NUM_EPS - rC = yLocs ** 2 + zC ** 2 - core_shape = 1 - np.exp(-rC / (eps ** 2)) - v_core = (Gamma_wake_rotation * zC) / (2 * np.pi * rC) * core_shape + rC = ne.evaluate("yLocs ** 2 + zC ** 2") + core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") + v_core = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape") v_core = np.mean( v_core, axis=(3,4) ) - # w_core = (-1 * Gamma_wake_rotation * yLocs) / (2 * np.pi * rC) * core_shape * decay + # w_core = (-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay # Cap the effective yaw values between -45 and 45 degrees val = 2 * (avg_v - v_core) / (v_top + v_bottom) val = np.where(val < -1.0, -1.0, val) val = np.where(val > 1.0, 1.0, val) - y = np.degrees( 0.5 * np.arcsin( val ) ) + y = np.degrees(0.5 * np.arcsin(val)) - return y[:,:,:,None,None] + return y[:, :, :, None, None] def calculate_transverse_velocity( @@ -358,7 +360,8 @@ def calculate_transverse_velocity( ct_i, tsr_i, axial_induction_i, - scale=1.0 + wind_shear, + scale=1.0, ): """ Calculate transverse velocity components for all downstream turbines @@ -373,14 +376,12 @@ def calculate_transverse_velocity( aI = axial_induction_i # flow parameters - Uinf = np.mean(u_initial, axis=(2,3,4)) - Uinf = Uinf[:,:,None,None,None] + Uinf = np.mean(u_initial, axis=(2, 3, 4))[:, :, None, None, None] eps_gain = 0.2 eps = eps_gain * D # Use set value - # TODO: wind sheer is hard-coded here but should be connected to the input - vel_top = ((HH + D / 2) / HH) ** 0.12 * np.ones((1, 1, 1, 1, 1)) + vel_top = ((HH + D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1, 1)) Gamma_top = sind(yaw) * cosd(yaw) * gamma( D, vel_top, @@ -389,7 +390,7 @@ def calculate_transverse_velocity( scale, ) - vel_bottom = ((HH - D / 2) / HH) ** 0.12 * np.ones((1, 1, 1, 1, 1)) + vel_bottom = ((HH - D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1, 1)) Gamma_bottom = -1 * sind(yaw) * cosd(yaw) * gamma( D, vel_bottom, @@ -397,10 +398,8 @@ def calculate_transverse_velocity( Ct, scale, ) - - turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(3,4))) - turbine_average_velocity = turbine_average_velocity[:,:,:,None,None] - Gamma_wake_rotation = 0.25 * 2 * np.pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR + turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(3, 4)))[:, :, :, None, None] + Gamma_wake_rotation = 0.25 * 2 * pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR ### compute the spanwise and vertical velocities induced by yaw @@ -410,75 +409,79 @@ def calculate_transverse_velocity( lm = kappa * z / (1 + kappa * z / lmda) nu = lm ** 2 * np.abs(dudz_initial) - decay = eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2) # This is the decay downstream + # This is the decay downstream + decay = ne.evaluate("eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2)") yLocs = delta_y + BaseModel.NUM_EPS # top vortex zT = z - (HH + D / 2) + BaseModel.NUM_EPS - rT = yLocs ** 2 + zT ** 2 # TODO: This is - in the paper + rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is - in the paper # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = 1 - np.exp(-rT / (eps ** 2)) - V1 = (Gamma_top * zT) / (2 * np.pi * rT) * core_shape * decay - W1 = (-1 * Gamma_top * yLocs) / (2 * np.pi * rT) * core_shape * decay + core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") + V1 = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape * decay") + W1 = ne.evaluate("(-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay") # bottom vortex zB = z - (HH - D / 2) + BaseModel.NUM_EPS - rB = yLocs ** 2 + zB ** 2 - core_shape = 1 - np.exp(-rB / (eps ** 2)) - V2 = (Gamma_bottom * zB) / (2 * np.pi * rB) * core_shape * decay - W2 = (-1 * Gamma_bottom * yLocs) / (2 * np.pi * rB) * core_shape * decay + rB = ne.evaluate("yLocs ** 2 + zB ** 2") + core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") + V2 = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape * decay") + W2 = ne.evaluate("(-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay") # wake rotation vortex zC = z - HH + BaseModel.NUM_EPS - rC = yLocs ** 2 + zC ** 2 - core_shape = 1 - np.exp(-rC / (eps ** 2)) - V5 = (Gamma_wake_rotation * zC) / (2 * np.pi * rC) * core_shape * decay - W5 = (-1 * Gamma_wake_rotation * yLocs) / (2 * np.pi * rC) * core_shape * decay - + rC = ne.evaluate("yLocs ** 2 + zC ** 2") + core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") + V5 = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape * decay") + W5 = ne.evaluate("(-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay") ### Boundary condition - ground mirror vortex # top vortex - ground zTb = z + (HH + D / 2) + BaseModel.NUM_EPS - rTb = yLocs ** 2 + zTb ** 2 + rTb = ne.evaluate("yLocs ** 2 + zTb ** 2") # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = 1 - np.exp(-rTb / (eps ** 2)) - V3 = (-1 * Gamma_top * zTb) / (2 * np.pi * rTb) * core_shape * decay - W3 = (Gamma_top * yLocs) / (2 * np.pi * rTb) * core_shape * decay + core_shape = ne.evaluate("1 - exp(-rTb / (eps ** 2))") + V3 = ne.evaluate("(-1 * Gamma_top * zTb) / (2 * pi * rTb) * core_shape * decay") + W3 = ne.evaluate("(Gamma_top * yLocs) / (2 * pi * rTb) * core_shape * decay") # bottom vortex - ground zBb = z + (HH - D / 2) + BaseModel.NUM_EPS - rBb = yLocs ** 2 + zBb ** 2 - core_shape = 1 - np.exp(-rBb / (eps ** 2)) - V4 = (-1 * Gamma_bottom * zBb) / (2 * np.pi * rBb) * core_shape * decay - W4 = (Gamma_bottom * yLocs) / (2 * np.pi * rBb) * core_shape * decay + rBb = ne.evaluate("yLocs ** 2 + zBb ** 2") + core_shape = ne.evaluate("1 - exp(-rBb / (eps ** 2))") + V4 = ne.evaluate("(-1 * Gamma_bottom * zBb) / (2 * pi * rBb) * core_shape * decay") + W4 = ne.evaluate("(Gamma_bottom * yLocs) / (2 * pi * rBb) * core_shape * decay") # wake rotation vortex - ground effect zCb = z + HH + BaseModel.NUM_EPS - rCb = yLocs ** 2 + zCb ** 2 - core_shape = 1 - np.exp(-rCb / (eps ** 2)) - V6 = (-1 * Gamma_wake_rotation * zCb) / (2 * np.pi * rCb) * core_shape * decay - W6 = (Gamma_wake_rotation * yLocs) / (2 * np.pi * rCb) * core_shape * decay + rCb = ne.evaluate("yLocs ** 2 + zCb ** 2") + core_shape = ne.evaluate("1 - exp(-rCb / (eps ** 2))") + V6 = ne.evaluate("(-1 * Gamma_wake_rotation * zCb) / (2 * pi * rCb) * core_shape * decay") + W6 = ne.evaluate("(Gamma_wake_rotation * yLocs) / (2 * pi * rCb) * core_shape * decay") # total spanwise velocity V = V1 + V2 + V3 + V4 + V5 + V6 W = W1 + W2 + W3 + W4 + W5 + W6 - # no spanwise and vertical velocity upstream of the turbine + # No spanwise and vertical velocity upstream of the turbine + ### Original v3 implementation # V[delta_x < -1] = 0.0 # Subtract by 1 to avoid numerical issues on rotation # W[delta_x < -1] = 0.0 # Subtract by 1 to avoid numerical issues on rotation # TODO Should this be <= ? Shouldn't be adding V and W on the current turbine? - V[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation - W[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation + ### Then we changed it to this + # V[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation + # W[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation + ### Currently, here + V = np.where(delta_x >= 0.0, V, 0.0) + W = np.where(delta_x >= 0.0, W, 0.0) # TODO: Why would the say W cannot be negative? - W[W < 0] = 0 + W = np.where(W >= 0, W, 0.0) return V, W - def yaw_added_turbulence_mixing( u_i, I_i, @@ -491,64 +494,24 @@ def yaw_added_turbulence_mixing( # use the left two dimensions only here and expand # before returning. Dimensions are (wd, ws). - I_i = I_i[:,:,0,0,0] + I_i = I_i[:, :, 0, 0, 0] - average_u_i = np.cbrt(np.mean(u_i ** 3, axis=(2,3,4))) + average_u_i = np.cbrt(np.mean(u_i ** 3, axis=(2, 3, 4))) # Convert ambient turbulence intensity to TKE (eq 24) k = (average_u_i * I_i) ** 2 / (2 / 3) u_term = np.sqrt(2 * k) - v_term = np.mean(v_i + turb_v_i, axis=(2,3,4)) - w_term = np.mean(w_i + turb_w_i, axis=(2,3,4)) + v_term = np.mean(v_i + turb_v_i, axis=(2, 3, 4)) + w_term = np.mean(w_i + turb_w_i, axis=(2, 3, 4)) # Compute the new TKE (eq 23) - k_total = 0.5 * ( u_term ** 2 + v_term ** 2 + w_term ** 2 ) + k_total = 0.5 * (u_term ** 2 + v_term ** 2 + w_term ** 2) # Convert TKE back to TI - I_total = np.sqrt( (2 / 3) * k_total ) / average_u_i + I_total = np.sqrt((2 / 3) * k_total) / average_u_i # Remove ambient from total TI leaving only the TI due to mixing I_mixing = I_total - I_i - return I_mixing[:,:,None,None,None] - -# def yaw_added_recovery_correction( -# self, U_local, U, W, x_locations, y_locations, turbine, turbine_coord -# ): -# """ -# This method corrects the U-component velocities when yaw added recovery -# is enabled. For more details on how the velocities are changed, see [1]. -# # TODO add reference to 1 - -# Args: -# U_local (np.array): U-component velocities across the flow field. -# U (np.array): U-component velocity deficits across the flow field. -# W (np.array): W-component velocity deficits across the flow field. -# x_locations (np.array): Streamwise locations in wake. -# y_locations (np.array): Spanwise locations in wake. -# turbine (:py:class:`floris.simulation.turbine.Turbine`): -# Turbine object. -# turbine_coord (:py:obj:`floris.simulation.turbine_map.TurbineMap.coords`): -# Spatial coordinates of wind turbine. - -# Returns: -# np.array: U-component velocity deficits across the flow field. -# """ -# # compute the velocity without modification -# U1 = U_local - U - -# # set dimensions -# D = turbine.rotor_diameter -# xLocs = x_locations - turbine_coord.x1 -# ky = self.ka * turbine.turbulence_intensity + self.kb -# U2 = (np.mean(W) * xLocs) / ((ky * xLocs + D / 2)) -# U_total = U1 + np.nan_to_num(U2) - -# # turn it back into a deficit -# U = U_local - U_total - -# # zero out anything before the turbine -# U[x_locations < turbine_coord.x1] = 0 - -# return U + return I_mixing[:, :, None, None, None] diff --git a/floris/simulation/wake_turbulence/crespo_hernandez.py b/floris/simulation/wake_turbulence/crespo_hernandez.py index e15504bee..923b62c6a 100644 --- a/floris/simulation/wake_turbulence/crespo_hernandez.py +++ b/floris/simulation/wake_turbulence/crespo_hernandez.py @@ -12,6 +12,7 @@ from typing import Any, Dict +import numexpr as ne import numpy as np from attrs import define, field @@ -55,6 +56,7 @@ class CrespoHernandez(BaseModel): :filter: docname in docnames :keyprefix: cht- """ + initial: float = field(converter=float, default=0.1) constant: float = field(converter=float, default=0.9) ai: float = field(converter=float, default=0.8) @@ -72,21 +74,25 @@ def function( axial_induction: np.ndarray, ) -> None: # Replace zeros and negatives with 1 to prevent nans/infs - delta_x = np.array(x - x_i) + delta_x = x - x_i # TODO: ensure that these fudge factors are needed for different rotations - upstream_mask = np.array(delta_x <= 0.1) - downstream_mask = np.array(delta_x > -0.1) + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 # Keep downstream components Set upstream to 1.0 - delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * np.array(upstream_mask) + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask # turbulence intensity calculation based on Crespo et. al. - ti = ( - self.constant - * axial_induction ** self.ai - * ambient_TI ** self.initial - * ((delta_x) / rotor_diameter) ** self.downstream + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter) ** downstream" ) # Mask the 1 values from above with zeros - return ti * np.array(downstream_mask) + return ti * downstream_mask diff --git a/floris/simulation/wake_velocity/cumulative_gauss_curl.py b/floris/simulation/wake_velocity/cumulative_gauss_curl.py index bc73ab2a7..7c603f5d3 100644 --- a/floris/simulation/wake_velocity/cumulative_gauss_curl.py +++ b/floris/simulation/wake_velocity/cumulative_gauss_curl.py @@ -95,58 +95,58 @@ def function( turbine_yaw = yaw_i # TODO Should this be cbrt? This is done to match v2 - turb_avg_vels = np.cbrt(np.mean(u_i ** 3, axis=(3,4))) - turb_avg_vels = turb_avg_vels[:,:,:,None,None] + turb_avg_vels = np.cbrt(np.mean(u_i ** 3, axis=(3, 4))) + turb_avg_vels = turb_avg_vels[:, :, :, None, None] delta_x = x - x_i sigma_n = wake_expansion( delta_x, - turbine_Ct[:,:,ii:ii+1], - turbine_ti[:,:,ii:ii+1], - turbine_diameter[:,:,ii:ii+1], + turbine_Ct[:, :, ii:ii+1], + turbine_ti[:, :, ii:ii+1], + turbine_diameter[:, :, ii:ii+1], self.a_s, self.b_s, self.c_s1, self.c_s2, ) - x_i_loc = np.mean(x_i, axis=(3,4)) - x_i_loc = x_i_loc[:,:,:,None,None] + x_i_loc = np.mean(x_i, axis=(3, 4)) + x_i_loc = x_i_loc[:, :, :, None, None] - y_i_loc = np.mean(y_i, axis=(3,4)) - y_i_loc = y_i_loc[:,:,:,None,None] + y_i_loc = np.mean(y_i, axis=(3, 4)) + y_i_loc = y_i_loc[:, :, :, None, None] - z_i_loc = np.mean(z_i, axis=(3,4)) - z_i_loc = z_i_loc[:,:,:,None,None] + z_i_loc = np.mean(z_i, axis=(3, 4)) + z_i_loc = z_i_loc[:, :, :, None, None] - x_coord = np.mean(x, axis=(3,4))[:,:,:,None,None] + x_coord = np.mean(x, axis=(3, 4))[:, :, :, None, None] y_loc = y - y_coord = np.mean(y, axis=(3,4))[:,:,:,None,None] + y_coord = np.mean(y, axis=(3, 4))[:, :, :, None, None] - z_loc = z # np.mean(z, axis=(3,4)) - z_coord = np.mean(z, axis=(3,4))[:,:,:,None,None] + z_loc = z # np.mean(z, axis=(3,4)) + z_coord = np.mean(z, axis=(3, 4))[:, :, :, None, None] sum_lbda = np.zeros_like(u_initial) for m in range(0, ii - 1): - x_coord_m = x_coord[:,:,m:m+1] - y_coord_m = y_coord[:,:,m:m+1] - z_coord_m = z_coord[:,:,m:m+1] + x_coord_m = x_coord[:, :, m:m+1] + y_coord_m = y_coord[:, :, m:m+1] + z_coord_m = z_coord[:, :, m:m+1] # For computing crossplanes, we don't need to compute downstream # turbines from out crossplane position. - if x_coord[:,:,m:m+1].size == 0: + if x_coord[:, :, m:m+1].size == 0: break delta_x_m = x - x_coord_m sigma_i = wake_expansion( delta_x_m, - turbine_Ct[:,:,m:m+1], - turbine_ti[:,:,m:m+1], - turbine_diameter[:,:,m:m+1], + turbine_Ct[:, :, m:m+1], + turbine_ti[:, :, m:m+1], + turbine_diameter[:, :, m:m+1], self.a_s, self.b_s, self.c_s1, @@ -191,7 +191,7 @@ def function( # based on Blondel model, modified to include cumulative effects tmp = a2 - ( - (n * turbine_Ct[:,:,ii:ii+1]) + (n * turbine_Ct[:, :, ii:ii+1]) * cosd(turbine_yaw) / ( 16.0 @@ -204,7 +204,7 @@ def function( # for some low wind speeds, tmp can become slightly negative, which causes NANs, # so replace the slightly negative values with zeros - tmp = tmp * np.array(tmp >= 0) + tmp = tmp * (tmp >= 0) C = a1 - np.sqrt(tmp) @@ -218,13 +218,10 @@ def function( # add turbines together velDef = C * np.exp((-1 * r_tilde ** n) / (2 * sigma_n ** 2)) - velDef = velDef * np.array(x - xR >= 0.1) + velDef = velDef * (x - xR >= 0.1) turb_u_wake = turb_u_wake + turb_avg_vels * velDef - return ( - turb_u_wake, - Ctmp, - ) + return (turb_u_wake, Ctmp) def wake_expansion( diff --git a/floris/simulation/wake_velocity/empirical_gauss.py b/floris/simulation/wake_velocity/empirical_gauss.py index 7a4d344a8..517ebf73f 100644 --- a/floris/simulation/wake_velocity/empirical_gauss.py +++ b/floris/simulation/wake_velocity/empirical_gauss.py @@ -65,7 +65,7 @@ class EmpiricalGaussVelocityDeficit(BaseModel): :style: unsrt :filter: docname in docnames """ - wake_expansion_rates: list = field(default=[0.01, 0.005]) + wake_expansion_rates: list = field(default=[0.023, 0.008]) breakpoints_D: list = field(default=[10]) sigma_0_D: float = field(default=0.28) smoothing_length_D: float = field(default=2.0) @@ -155,8 +155,8 @@ def function( sigma_z0 = self.sigma_0_D * rotor_diameter_i * cosd(tilt_angle_i) # No specific near, far wakes in this model - downstream_mask = np.array(x > x_i + 0.1) - upstream_mask = np.array(x < x_i - 0.1) + downstream_mask = (x > x_i + 0.1) + upstream_mask = (x < x_i - 0.1) # Wake expansion in the lateral (y) and the vertical (z) # TODO: could compute shared components in sigma_z, sigma_y diff --git a/floris/simulation/wake_velocity/gauss.py b/floris/simulation/wake_velocity/gauss.py index f8061bfc9..12b82b7b5 100644 --- a/floris/simulation/wake_velocity/gauss.py +++ b/floris/simulation/wake_velocity/gauss.py @@ -73,7 +73,7 @@ def function( y: np.ndarray, z: np.ndarray, u_initial: np.ndarray, - wind_veer: float + wind_veer: float, ) -> None: # yaw_angle is all turbine yaw angles for each wind speed @@ -84,14 +84,13 @@ def function( yaw_angle = -1 * yaw_angle_i # Initialize the velocity deficit - uR = u_initial * ct_i / ( 2.0 * (1 - np.sqrt(1 - ct_i) ) ) + uR = u_initial * ct_i / (2.0 * (1 - np.sqrt(1 - ct_i))) u0 = u_initial * np.sqrt(1 - ct_i) # Initial lateral bounds sigma_z0 = rotor_diameter_i * 0.5 * np.sqrt(uR / (u_initial + u0)) sigma_y0 = sigma_z0 * cosd(yaw_angle) * cosd(wind_veer) - # Compute the bounds of the near and far wake regions and a mask # Start of the near wake @@ -115,8 +114,8 @@ def function( # zero value. # This mask defines the near wake; keeps the areas downstream of xR and upstream of x0 - near_wake_mask = np.array(x > xR + 0.1) * np.array(x < x0) - far_wake_mask = np.array(x >= x0) + near_wake_mask = (x > xR + 0.1) * (x < x0) + far_wake_mask = (x >= x0) # Compute the velocity deficit in the NEAR WAKE region # ONLY If there are points within the near wake boundary @@ -136,13 +135,13 @@ def function( sigma_y = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt(ct_i / 2.0) sigma_y += near_wake_ramp_up * sigma_y0 - sigma_y *= np.array(x >= xR) - sigma_y += np.ones_like(sigma_y) * np.array(x < xR) * 0.5 * rotor_diameter_i + sigma_y *= (x >= xR) + sigma_y += np.ones_like(sigma_y) * (x < xR) * 0.5 * rotor_diameter_i sigma_z = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt(ct_i / 2.0) sigma_z += near_wake_ramp_up * sigma_z0 - sigma_z *= np.array(x >= xR) - sigma_z += np.ones_like(sigma_z) * np.array(x < xR) * 0.5 * rotor_diameter_i + sigma_z *= (x >= xR) + sigma_z += np.ones_like(sigma_z) * (x < xR) * 0.5 * rotor_diameter_i r, C = rC( wind_veer, @@ -155,7 +154,7 @@ def function( hub_height_i, ct_i, yaw_angle, - rotor_diameter_i + rotor_diameter_i, ) near_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) @@ -163,15 +162,14 @@ def function( velocity_deficit += near_wake_deficit - # Compute the velocity deficit in the FAR WAKE region if np.sum(far_wake_mask): # Wake expansion in the lateral (y) and the vertical (z) ky = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters kz = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters - sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * np.array(x < x0) - sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * np.array(x < x0) + sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * (x < x0) + sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * (x < x0) r, C = rC( wind_veer, @@ -184,7 +182,7 @@ def function( hub_height_i, ct_i, yaw_angle, - rotor_diameter_i + rotor_diameter_i, ) far_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) @@ -238,10 +236,13 @@ def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): C = ne.evaluate("1 - sqrt(d)") return r, C + def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine_yaw): yR = mesh_y_rotated - y_coord_rotated xR = yR * tand(turbine_yaw) + x_coord_rotated return xR, yR + def gaussian_function(C, r, n, sigma): - return C * np.exp(-1 * r ** n / (2 * sigma ** 2)) + result = ne.evaluate("C * exp(-1 * r ** n / (2 * sigma ** 2))") + return result diff --git a/floris/simulation/wake_velocity/jensen.py b/floris/simulation/wake_velocity/jensen.py index 4b74bdddb..d485fecf3 100644 --- a/floris/simulation/wake_velocity/jensen.py +++ b/floris/simulation/wake_velocity/jensen.py @@ -101,40 +101,6 @@ def function( rotor_radius = rotor_diameter_i / 2.0 - """ - dx = x - x_i - dy = y - y_i - deflection_field_i - dz = z - z_i - - # y = m * x + b - boundary_line = self.we * dx + rotor_radius - - # Calculate the wake velocity deficit ratios - # Do we need to do masking here or can it be handled in the solver? - # TODO: why do we need to slice with i:i+1 below? This became a problem - # when adding the wind direction dimension. Prior to that, the dimensions - # worked out simply with i. - c = ( rotor_radius / ( rotor_radius + self.we * dx + self.NUM_EPS ) ) ** 2 - - # using this causes nan's in the upstream turbine because it negates the mask - # rather than setting it to 0. When self.we * (x - x[:, :, i:i+1]) ) == the radius, - # c goes to infinity and then this line flips it to Nans rather than setting to 0. - # c *= ~(np.array(x - x[:, :, i:i+1] <= 0.0)) - # c *= ~(((y - y_center) ** 2 + (z - z_center) ** 2) > (boundary_line ** 2)) - # np.nan_to_num - - # C should be 0 at the current turbine and everywhere in front of it - downstream_mask = np.array(dx > 0.0 + self.NUM_EPS, dtype=int) - # C should be 0 everywhere outside of the lateral and vertical bounds defined by - # the wake expansion parameter - boundary_mask = np.array( np.sqrt(dy ** 2 + dz ** 2) < boundary_line, dtype=int) - - mask = np.logical_and(downstream_mask, boundary_mask) - c[~mask] = 0.0 - - velocity_deficit = 2 * axial_induction_i * c - """ - # Numexpr - do not change below without corresponding changes above. dx = ne.evaluate("x - x_i") dy = ne.evaluate("y - y_i - deflection_field_i") @@ -143,21 +109,24 @@ def function( we = self.we NUM_EPS = JensenVelocityDeficit.NUM_EPS - # y = m * x + b - boundary_line = ne.evaluate("we * dx + rotor_radius") - - c = ne.evaluate("( rotor_radius / ( rotor_radius + we * dx + NUM_EPS ) ) ** 2") - - # C should be 0 at the current turbine and everywhere in front of it + # Construct a boolean mask to include all points downstream of the turbine downstream_mask = ne.evaluate("dx > 0 + NUM_EPS") - # C should be 0 everywhere outside of the lateral and vertical bounds defined - # by the wake expansion parameter - boundary_mask = ne.evaluate("sqrt(dy ** 2 + dz ** 2) < boundary_line") - - mask = np.logical_and(downstream_mask, boundary_mask) - c[~mask] = 0.0 - # c = ne.evaluate("c * downstream_mask * boundary_mask") + # Construct a boolean mask to include all points within the wake boundary + # as defined by the Jensen model. This is a linear wake expansion that makes + # a shape like a cone and starts at the turbine disc. + # The left side of the inequality below evaluates the distance from the wake centerline + # for all points including positive and negative values. The inequality compares distance + # from the centerline and it must be below the line defined by the wake + # expansion parameter, "we". + boundary_mask = ne.evaluate("sqrt(dy ** 2 + dz ** 2) < we * dx + rotor_radius") + + # Calculate C for points within the mask and fill points outside with 0 + c = np.where( + np.logical_and(downstream_mask, boundary_mask), + ne.evaluate("(rotor_radius / (rotor_radius + we * dx + NUM_EPS)) ** 2"), # This is "C" + 0.0, + ) velocity_deficit = ne.evaluate("2 * axial_induction_i * c") diff --git a/floris/simulation/wake_velocity/turbopark.py b/floris/simulation/wake_velocity/turbopark.py index e98e212f1..f66a447a9 100644 --- a/floris/simulation/wake_velocity/turbopark.py +++ b/floris/simulation/wake_velocity/turbopark.py @@ -42,6 +42,7 @@ class TurbOParkVelocityDeficit(BaseModel): Nygaard, Nicolai Gayle, et al. "Modelling cluster wakes and wind farm blockage." Journal of Physics: Conference Series. Vol. 1618. No. 6. IOP Publishing, 2020. """ + A: float = field(default=0.04) sigma_max_rel: float = field(default=4.0) overlap_gauss_interp: RegularGridInterpolator = field(init=False) @@ -100,7 +101,7 @@ def function( # subsequent runtime warnings. # Here self.NUM_EPS is to avoid precision issues with masking, and is slightly # larger than 0.0 - downstream_mask = np.array(x_i - x >= self.NUM_EPS) + downstream_mask = (x_i - x >= self.NUM_EPS) x_dist = (x_i - x) * downstream_mask / rotor_diameters # Radial distance between turbine i and the centerlines of wakes from all @@ -108,7 +109,7 @@ def function( r_dist = np.sqrt((y_i - (y + deflection_field)) ** 2 + (z_i - z) ** 2) r_dist_image = np.sqrt((y_i - (y + deflection_field)) ** 2 + (z_i - (-z)) ** 2) - Cts[:,:,i:,:,:] = 0.00001 + Cts[:, :, i:, :, :] = 0.00001 # Characteristic wake widths from all turbines relative to turbine i dw = characteristic_wake_width(x_dist, ambient_turbulence_intensity, Cts, self.A) @@ -122,10 +123,9 @@ def function( C = 1 - np.sqrt(val) # Compute deficit for all turbines and mask to keep upstream and overlapping turbines - effective_width = self.sigma_max_rel * sigma - is_overlapping = effective_width / 2 + rotor_diameter_i / 2 > r_dist - - wtg_overlapping = np.array(x_dist > 0) * is_overlapping + # NOTE self.sigma_max_rel * sigma is an effective wake width + is_overlapping = (self.sigma_max_rel * sigma) / 2 + rotor_diameter_i / 2 > r_dist + wtg_overlapping = (x_dist > 0) * is_overlapping delta_real = np.empty(np.shape(u_initial)) * np.nan delta_image = np.empty(np.shape(u_initial)) * np.nan @@ -139,7 +139,7 @@ def function( ) delta = np.concatenate((delta_real, delta_image), axis=2) - delta_total[:, :, i, :, :] = np.sqrt(np.sum(np.nan_to_num(delta)**2, axis=2)) + delta_total[:, :, i, :, :] = np.sqrt(np.sum(np.nan_to_num(delta) ** 2, axis=2)) return delta_total diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 4e4cc352e..c5404d0b2 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -29,6 +29,7 @@ power, rotor_effective_velocity, ) +from floris.simulation.turbine_multi_dim import multidim_power_down_select, power_multidim from floris.tools.cut_plane import CutPlane from floris.type_dec import NDArrayFloat @@ -586,7 +587,7 @@ def get_turbine_powers(self) -> NDArrayFloat: """Calculates the power at each turbine in the windfarm. Returns: - NDArrayFloat: [description] + NDArrayFloat: Powers at each turbine. """ # Confirm calculate wake has been run @@ -595,6 +596,10 @@ def get_turbine_powers(self) -> NDArrayFloat: "Can't run function `FlorisInterface.get_turbine_powers` without " "first running `FlorisInterface.calculate_wake`." ) + # Check for negative velocities, which could indicate bad model + # parameters or turbines very closely spaced. + if (self.turbine_effective_velocities < 0.).any(): + self.logger.warning("Some rotor effective velocities are negative.") turbine_powers = power( ref_density_cp_ct=self.floris.farm.ref_density_cp_cts, @@ -604,6 +609,37 @@ def get_turbine_powers(self) -> NDArrayFloat: ) return turbine_powers + def get_turbine_powers_multidim(self) -> NDArrayFloat: + """Calculates the power at each turbine in the windfarm + when using multi-dimensional Cp/Ct turbine definitions. + + Returns: + NDArrayFloat: Powers at each turbine. + """ + + # Confirm calculate wake has been run + if self.floris.state is not State.USED: + raise RuntimeError( + "Can't run function `FlorisInterface.get_turbine_powers_multidim` without " + "first running `FlorisInterface.calculate_wake`." + ) + # Check for negative velocities, which could indicate bad model + # parameters or turbines very closely spaced. + if (self.turbine_effective_velocities < 0.).any(): + self.logger.warning("Some rotor effective velocities are negative.") + + turbine_power_interps = multidim_power_down_select( + self.floris.farm.turbine_power_interps, + self.floris.flow_field.multidim_conditions + ) + + turbine_powers = power_multidim( + ref_density_cp_ct=self.floris.farm.ref_density_cp_cts, + rotor_effective_velocities=self.turbine_effective_velocities, + power_interp=turbine_power_interps, + ) + return turbine_powers + def get_turbine_Cts(self) -> NDArrayFloat: turbine_Cts = Ct( velocities=self.floris.flow_field.u, @@ -987,15 +1023,3 @@ def get_turbine_layout(self, z=False): return xcoords, ycoords, zcoords else: return xcoords, ycoords - - -## Functionality removed in v3 - -def set_rotor_diameter(self, rotor_diameter): - """ - This function has been replaced and no longer works correctly, assigning an error - """ - raise Exception( - "FlorinInterface.set_rotor_diameter has been removed in favor of " - "FlorinInterface.change_turbine. See examples/change_turbine/." - ) diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_base.py b/floris/tools/optimization/layout_optimization/layout_optimization_base.py index 6acb5b482..ea160e038 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_base.py @@ -16,13 +16,18 @@ import numpy as np from shapely.geometry import LineString, Polygon +from floris.tools.optimization.yaw_optimization.yaw_optimizer_geometric import ( + YawOptimizationGeometric, +) + from ....logging_manager import LoggerBase class LayoutOptimization(LoggerBase): - def __init__(self, fi, boundaries, min_dist=None, freq=None): + def __init__(self, fi, boundaries, min_dist=None, freq=None, enable_geometric_yaw=False): self.fi = fi.copy() self.boundaries = boundaries + self.enable_geometric_yaw = enable_geometric_yaw self._boundary_polygon = Polygon(self.boundaries) self._boundary_line = LineString(self.boundaries) @@ -48,6 +53,15 @@ def __init__(self, fi, boundaries, min_dist=None, freq=None): else: self.freq = freq + # Establish geometric yaw class + if self.enable_geometric_yaw: + self.yaw_opt = YawOptimizationGeometric( + fi, + minimum_yaw_angle=-30.0, + maximum_yaw_angle=30.0, + exploit_layout_symmetry=False + ) + self.initial_AEP = fi.get_farm_AEP(self.freq) def __str__(self): @@ -59,6 +73,18 @@ def _norm(self, val, x1, x2): def _unnorm(self, val, x1, x2): return np.array(val) * (x2 - x1) + x1 + def _get_geoyaw_angles(self): + # NOTE: requires that child class saves x and y locations + # as self.x and self.y and updates them during optimization. + if self.enable_geometric_yaw: + self.yaw_opt.fi_subset.reinitialize(layout_x=self.x, layout_y=self.y) + df_opt = self.yaw_opt.optimize() + self.yaw_angles = np.vstack(df_opt['yaw_angles_opt'])[:, None, :] + else: + self.yaw_angles = None + + return self.yaw_angles + # Public methods def optimize(self): @@ -95,7 +121,6 @@ def plot_layout_opt_results(self): [verts[i][0], verts[i + 1][0]], [verts[i][1], verts[i + 1][1]], "b" ) - plt.show() ########################################################################### # Properties diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py b/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py index dc2a50e8e..714387ffc 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py @@ -641,8 +641,6 @@ def plot_layout(self): plt.grid() plt.tick_params(which="both", labelsize=fontsize) - plt.show() - def space_constraint(self, x, y, min_dist, rho=500): # Calculate distances between turbines locs = np.vstack((x, y)).T diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py index 694c8e717..5539b84a0 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py @@ -32,9 +32,11 @@ def __init__( optOptions=None, timeLimit=None, storeHistory='hist.hist', - hotStart=None + hotStart=None, + enable_geometric_yaw=False, ): - super().__init__(fi, boundaries, min_dist=min_dist, freq=freq) + super().__init__(fi, boundaries, min_dist=min_dist, freq=freq, + enable_geometric_yaw=enable_geometric_yaw) self.x0 = self._norm(self.fi.layout_x, self.xmin, self.xmax) self.y0 = self._norm(self.fi.layout_y, self.ymin, self.ymax) @@ -42,6 +44,7 @@ def __init__( self.storeHistory = storeHistory self.timeLimit = timeLimit self.hotStart = hotStart + self.enable_geometric_yaw = enable_geometric_yaw try: import pyoptsparse @@ -105,10 +108,13 @@ def _obj_func(self, varDict): # Update turbine map with turbince locations self.fi.reinitialize(layout_x = self.x, layout_y = self.y) + # Compute turbine yaw angles using PJ's geometric code (if enabled) + yaw_angles = self._get_geoyaw_angles() + # Compute the objective function funcs = {} funcs["obj"] = ( - -1 * self.fi.get_farm_AEP(self.freq) / self.initial_AEP + -1 * self.fi.get_farm_AEP(self.freq, yaw_angles=yaw_angles) / self.initial_AEP ) # Compute constraints, if any are defined for the optimization diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py index 9b04cb2d9..d4ff29c35 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py @@ -226,5 +226,3 @@ def plot_layout_opt_results(self): plt.plot( [verts[i][0], verts[i + 1][0]], [verts[i][1], verts[i + 1][1]], "b" ) - - plt.show() diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py b/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py index 61b0df8f3..d8f3fa2d5 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py @@ -31,6 +31,7 @@ def __init__( min_dist=None, solver='SLSQP', optOptions=None, + enable_geometric_yaw=False, ): """ _summary_ @@ -53,7 +54,8 @@ def __init__( optOptions (dict, optional): Dicitonary for setting the optimization options. Defaults to None. """ - super().__init__(fi, boundaries, min_dist=min_dist, freq=freq) + super().__init__(fi, boundaries, min_dist=min_dist, freq=freq, + enable_geometric_yaw=enable_geometric_yaw) self.boundaries_norm = [ [ @@ -75,10 +77,12 @@ def __init__( self._set_opt_bounds() if solver is not None: self.solver = solver + + default_optOptions = {"maxiter": 100, "disp": True, "iprint": 2, "ftol": 1e-9, "eps":0.01} if optOptions is not None: - self.optOptions = optOptions + self.optOptions = {**default_optOptions, **optOptions} else: - self.optOptions = {"maxiter": 100, "disp": True, "iprint": 2, "ftol": 1e-9, "eps":0.01} + self.optOptions = default_optOptions self._generate_constraints() @@ -106,13 +110,20 @@ def _obj_func(self, locs): for valy in locs[self.nturbs : 2 * self.nturbs] ] self._change_coordinates(locs_unnorm) - return -1 * self.fi.get_farm_AEP(self.freq) / self.initial_AEP + # Compute turbine yaw angles using PJ's geometric code (if enabled) + yaw_angles = self._get_geoyaw_angles() + return (-1 * self.fi.get_farm_AEP(self.freq, yaw_angles=yaw_angles) / + self.initial_AEP) def _change_coordinates(self, locs): # Parse the layout coordinates layout_x = locs[0 : self.nturbs] layout_y = locs[self.nturbs : 2 * self.nturbs] + # Store on object for use in geoyaw code + self.x = layout_x + self.y = layout_y + # Update the turbine map in floris self.fi.reinitialize(layout_x=layout_x, layout_y=layout_y) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index ddddff55a..b0b9ddeaf 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -19,10 +19,12 @@ import numpy as np import pandas as pd +from floris.logging_manager import LoggerBase + from .yaw_optimization_tools import derive_downstream_turbines, find_layout_symmetry -class YawOptimization: +class YawOptimization(LoggerBase): """ YawOptimization is a subclass of :py:class:`floris.tools.optimization.scipy. Optimization` that is used to optimize the yaw angles of all turbines in a Floris @@ -176,7 +178,17 @@ def __init__( self.normalize_variables = normalize_control_variables self.calc_baseline_power = calc_baseline_power self.exclude_downstream_turbines = exclude_downstream_turbines - self.exploit_layout_symmetry = exploit_layout_symmetry + + # Check if exploit_layout_symmetry is being used with heterogeneous inflow + if exploit_layout_symmetry and fi.floris.flow_field.heterogenous_inflow_config is not None: + err_msg = ( + "Layout symmetry cannot be exploited with heterogeneous inflows. " + "Setting exploit_layout_symmetry to False." + ) + self.logger.warning(err_msg, stack_info=True) + self.exploit_layout_symmetry = False + else: + self.exploit_layout_symmetry = exploit_layout_symmetry # Prepare for optimization and calculate baseline powers (if applic.) self._initialize() @@ -338,7 +350,9 @@ def _normalize_control_problem(self): / self._normalization_length ) - def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights=None): + def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights=None, + heterogeneous_speed_multipliers=None + ): """ Calculate the wind farm power production assuming the predefined probability distribution (self.unc_options/unc_pmf), with the @@ -358,6 +372,9 @@ def _calculate_farm_power(self, yaw_angles=None, wd_array=None, turbine_weights= yaw_angles = self._yaw_angles_baseline_subset if turbine_weights is None: turbine_weights = self._turbine_weights_subset + if heterogeneous_speed_multipliers is not None: + fi_subset.floris.flow_field.\ + heterogenous_inflow_config['speed_multipliers'] = heterogeneous_speed_multipliers # Ensure format [incompatible with _subset notation] yaw_angles = self._unpack_variable(yaw_angles, subset=True) @@ -385,6 +402,9 @@ def _calculate_baseline_farm_power(self): P = self._calculate_farm_power(self._yaw_angles_baseline_subset) self._farm_power_baseline_subset = P self.farm_power_baseline = self._unreduce_variable(P) + else: + self._farm_power_baseline_subset = None + self.farm_power_baseline = None def _derive_layout_symmetry(self): """Derive symmetry lines in the wind farm layout and use that @@ -456,6 +476,9 @@ def _derive_layout_symmetry(self): def _unreduce_variable(self, variable): # Check if needed to un-reduce at all, if not, return directly + if variable is None: + return variable + if not self.exploit_layout_symmetry: return variable @@ -516,8 +539,10 @@ def _finalize(self, farm_power_opt_subset=None, yaw_angles_opt_subset=None): "wind_speed": wind_speed * np.ones(num_wind_directions), "turbulence_intensity": ti * np.ones(num_wind_directions), "yaw_angles_opt": list(self.yaw_angles_opt[:, ii, :]), - "farm_power_opt": self.farm_power_opt[:, ii], - "farm_power_baseline": self.farm_power_baseline[:, ii], + "farm_power_opt": None if self.farm_power_opt is None \ + else self.farm_power_opt[:, ii], + "farm_power_baseline": None if self.farm_power_baseline is None \ + else self.farm_power_baseline[:, ii], })) df_opt = pd.concat(df_list, axis=0) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimizer_geometric.py b/floris/tools/optimization/yaw_optimization/yaw_optimizer_geometric.py new file mode 100644 index 000000000..6c63b52fd --- /dev/null +++ b/floris/tools/optimization/yaw_optimization/yaw_optimizer_geometric.py @@ -0,0 +1,267 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +import numpy as np + +from floris.utilities import rotate_coordinates_rel_west + +from .yaw_optimization_base import YawOptimization + + +class YawOptimizationGeometric(YawOptimization): + """ + YawOptimizationGeometric is a subclass of + :py:class:`floris.tools.optimization.general_library.YawOptimization` that is + used to provide a rough estimate of optimal yaw angles based purely on the + wind farm geometry. Main use case is for coupled layout and yaw optimization. + """ + + def __init__( + self, + fi, + minimum_yaw_angle=0.0, + maximum_yaw_angle=25.0, + exploit_layout_symmetry=True, + ): + """ + Instantiate YawOptimizationGeometric object with a FlorisInterface + object assign parameter values. + """ + + super().__init__( + fi=fi, + minimum_yaw_angle=minimum_yaw_angle, + maximum_yaw_angle=maximum_yaw_angle, + exploit_layout_symmetry=exploit_layout_symmetry, + calc_baseline_power=False + ) + + def optimize(self): + """ + Find rough yaw angles based on wind farm geometry. + Assumes all wind turbines have the same rotor diameter. + + Returns: + opt_yaw_angles (np.array): Optimal yaw angles in degrees. This + array is equal in length to the number of turbines in the farm. + """ + # Loop through every WD individually. WS ignored! + wd_array = self.fi_subset.floris.flow_field.wind_directions + + for nwdi, wd in enumerate(wd_array): + self._yaw_angles_opt_subset[nwdi, :, :] = geometric_yaw( + self.fi_subset.layout_x, + self.fi_subset.layout_y, + wd, + self.fi.floris.farm.turbine_definitions[0]["rotor_diameter"], + top_left_yaw_upper=self.maximum_yaw_angle[0,0,0], + bottom_left_yaw_upper=self.maximum_yaw_angle[0,0,0], + top_left_yaw_lower=self.minimum_yaw_angle[0,0,0], + bottom_left_yaw_lower=self.minimum_yaw_angle[0,0,0] + ) + + # Finalize optimization, i.e., retrieve full solutions + df_opt = self._finalize() + + # Otherwise, df_opt will just copy the farm_power_baseline in + return df_opt + +def geometric_yaw( + turbine_x, + turbine_y, + wind_direction, + rotor_diameter, + left_x=0.0, + top_left_y=1.0, + right_x=25.0, + top_right_y=1.0, + top_left_yaw_upper=30.0, + top_right_yaw_upper=0.0, + bottom_left_yaw_upper=30.0, + bottom_right_yaw_upper=0.0, + top_left_yaw_lower=-30.0, + top_right_yaw_lower=0.0, + bottom_left_yaw_lower=-30.0, + bottom_right_yaw_lower=0.0 +): + """ + turbine_x: unrotated x turbine coords + turbine_y: unrotated y turbine coords + wind_direction: float, degrees + rotor_diameter: float + left_x: where we start the trapezoid. Should be left as 0. + top_left_y: trapezoid top left coord + right_x: where to stop the trapezoid downstream. + Max coord after which the upstream turbine won't yaw. + top_right_y: trapezoid top right coord + top_left_yaw_upper: yaw angle associated with top left point (upper trapezoid) + top_right_yaw_upper: yaw angle associated with top right point + bottom_left_yaw_upper: yaw angle associated with bottom left point + bottom_right_yaw_upper: yaw angle associated with bottom right point + top_left_yaw_lower: yaw angle associated with top left point (lower trapezoid) + top_right_yaw_lower: yaw angle associated with top right point + bottom_left_yaw_lower: yaw angle associated with bottom left point + bottom_right_yaw_lower: yaw angle associated with bottom right point + """ + + nturbs = len(turbine_x) + turbine_coordinates_array = np.zeros((nturbs,3)) + turbine_coordinates_array[:,0] = turbine_x[:] + turbine_coordinates_array[:,1] = turbine_y[:] + + rotated_x, rotated_y, _, _, _ = rotate_coordinates_rel_west( + np.array([wind_direction]), + turbine_coordinates_array + ) + processed_x, processed_y = _process_layout(rotated_x[0][0],rotated_y[0][0],rotor_diameter) + yaw_array = np.zeros(nturbs) + for i in range(nturbs): + # TODO: fix shape of top left yaw etc? + yaw_array[i] = _get_yaw_angles( + processed_x[i], + processed_y[i], + left_x, + top_left_y, + right_x, + top_right_y, + top_left_yaw_upper, + top_right_yaw_upper, + bottom_left_yaw_upper, + bottom_right_yaw_upper, + top_left_yaw_lower, + top_right_yaw_lower, + bottom_left_yaw_lower, + bottom_right_yaw_lower + ) + + return yaw_array + +def _process_layout( + turbine_x, + turbine_y, + rotor_diameter, + spread=0.1 +): + """ + returns the distance from each turbine to the nearest downstream waked turbine + normalized by the rotor diameter. Right now "waked" is determind by a Jensen-like + wake spread, but this could/should be modified to be the same as the trapezoid rule + used to determine the yaw angles. + + turbine_x: turbine x coords (rotated) + turbine_y: turbine y coords (rotated) + rotor_diameter: turbine rotor diameter (float) + spread=0.1: Jensen alpha wake spread value + """ + len(turbine_x) + + # # Intialize storage + # dx = np.zeros(nturbs) + 1E10 + # dy = np.zeros(nturbs) + + # for waking_index in range(nturbs): + # for waked_index in range(nturbs): + # if turbine_x[waked_index] > turbine_x[waking_index]: + # r = spread*(turbine_x[waked_index]-turbine_x[waking_index]) + rotor_diameter/2.0 + # if abs(turbine_y[waked_index]-turbine_y[waking_index]) < (r+rotor_diameter/2.0): + # if (turbine_x[waked_index] - turbine_x[waking_index]) < dx[waking_index]: + # dx[waking_index] = turbine_x[waked_index] - turbine_x[waking_index] + # dy[waking_index] = turbine_y[waked_index]- turbine_y[waking_index] + # if dx[waking_index] == 1E10: + # dx[waking_index] = 0.0 + + # dx_ = dx + # dy_ = dy + + # Compute distances + x_dists = turbine_x.reshape(-1,1).T - turbine_x.reshape(-1,1) + y_dists = turbine_y.reshape(-1,1).T - turbine_y.reshape(-1,1) + + # Any turbines upstream or at the turbine location are ineligble + x_dists[x_dists <= 0.] = np.inf + + # Check within Jensen model spread + in_Jensen_wake = (abs(y_dists) < spread * x_dists + rotor_diameter) + x_dists[~in_Jensen_wake] = np.inf + + # Get minimums (and arguments to select the correct y values also) + dx = x_dists.min(axis=1) + dy = y_dists[range(len(turbine_x)), x_dists.argmin(axis=1)] + + # Handle last turbine downstream + furthest_ds_turb_idx = np.where(dx == np.inf)[0] + dx[furthest_ds_turb_idx] = 0. + dy[furthest_ds_turb_idx] = 0. + + return dx/rotor_diameter, dy/rotor_diameter + + +def _get_yaw_angles( + x, + y, + left_x, + top_left_y, + right_x, + top_right_y, + top_left_yaw_upper, + top_right_yaw_upper, + bottom_left_yaw_upper, + bottom_right_yaw_upper, + top_left_yaw_lower, + top_right_yaw_lower, + bottom_left_yaw_lower, + bottom_right_yaw_lower +): + """ + _______2,5___________________________4,6 + |....................................... + |......1,7...........................3,8 + |....................................... + ________________________________________ + + x and y: dx and dy to the nearest downstream turbine in rotor diameteters with + turbines rotated so wind is coming left to right + left_x: where we start the trapezoid. Should be left as 0. + top_left_y: trapezoid top left coord + right_x: where to stop the trapezoid downstream. + Max coord after which the upstream turbine won't yaw. + top_right_y: trapezoid top right coord + top_left_yaw_upper: yaw angle associated with top left point (upper trapezoid) + top_right_yaw_upper: yaw angle associated with top right point + bottom_left_yaw_upper: yaw angle associated with bottom left point + bottom_right_yaw_upper: yaw angle associated with bottom right point + top_left_yaw_lower: yaw angle associated with top left point (lower trapezoid) + top_right_yaw_lower: yaw angle associated with top right point + bottom_left_yaw_lower: yaw angle associated with bottom left point + bottom_right_yaw_lower: yaw angle associated with bottom right point + """ + + if x <= 0: + return 0.0 + else: + dx = (x-left_x)/(right_x-left_x) + if dx >= 1.0: + return 0.0 + edge_y = top_left_y + (top_right_y-top_left_y)*dx + if abs(y) > edge_y: + return 0.0 + elif y >= -0.01: # Tolerance to handle numerical issues + top_yaw = top_left_yaw_upper + (top_right_yaw_upper-top_left_yaw_upper)*dx + bottom_yaw = bottom_left_yaw_upper + (bottom_right_yaw_upper-bottom_left_yaw_upper)*dx + return bottom_yaw + (top_yaw-bottom_yaw)*abs(y)/edge_y + elif y < -0.01: + top_yaw = top_left_yaw_lower + (top_right_yaw_lower-top_left_yaw_lower)*dx + bottom_yaw = bottom_left_yaw_lower + (bottom_right_yaw_lower-bottom_left_yaw_lower)*dx + return bottom_yaw + (top_yaw-bottom_yaw)*abs(y)/edge_y diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py index 81ffc32a3..66339e426 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -108,6 +108,16 @@ def optimize(self): yaw_template = np.tile(yaw_template, (1, 1, 1)) turbine_weights = np.tile(turbine_weights, (1, 1, 1)) + # Handle heterogeneous inflow, if there is one + if (hasattr(self.fi.floris.flow_field, 'heterogenous_inflow_config') and + self.fi.floris.flow_field.heterogenous_inflow_config is not None): + het_sm_orig = np.array( + self.fi.floris.flow_field.heterogenous_inflow_config['speed_multipliers'] + ) + het_sm = het_sm_orig[nwdi,:].reshape(1,-1) + else: + het_sm = None + # Define cost function def cost(x): x_full = np.array(yaw_template, copy=True) @@ -116,7 +126,8 @@ def cost(x): - 1.0 * self._calculate_farm_power( yaw_angles=x_full, wd_array=[wd], - turbine_weights=turbine_weights + turbine_weights=turbine_weights, + heterogeneous_speed_multipliers=het_sm )[0, 0] / J0 ) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/tools/optimization/yaw_optimization/yaw_optimizer_sr.py index 801c59312..82d50ef08 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -141,10 +141,19 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): if not np.all(idx): # Now calculate farm powers for conditions we haven't yet evaluated previously start_time = timerpc() + if (hasattr(self.fi.floris.flow_field, 'heterogenous_inflow_config') and + self.fi.floris.flow_field.heterogenous_inflow_config is not None): + het_sm_orig = np.array( + self.fi.floris.flow_field.heterogenous_inflow_config['speed_multipliers'] + ) + het_sm = np.tile(het_sm_orig, (Ny, 1))[~idx, :] + else: + het_sm = None farm_powers[~idx, :] = self._calculate_farm_power( wd_array=wd_array_subset[~idx], turbine_weights=turbine_weights_subset[~idx, :, :], yaw_angles=yaw_angles_subset[~idx, :, :], + heterogeneous_speed_multipliers=het_sm ) self.time_spent_in_floris += (timerpc() - start_time) diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index 600228a87..b1808ddb5 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -1,5 +1,6 @@ # Copyright 2022 Shell import copy +import warnings from time import perf_counter as timerpc import numpy as np @@ -72,7 +73,8 @@ def __init__( max_workers, n_wind_direction_splits, n_wind_speed_splits=1, - use_mpi4py=False, + interface="multiprocessing", # Options are 'multiprocessing', 'mpi4py' or 'concurrent' + use_mpi4py=None, propagate_flowfield_from_workers=False, print_timings=False ): @@ -83,17 +85,51 @@ def __init__( fi (FlorisInterface or UncertaintyInterface object): Interactive FLORIS object used to perform the wake and turbine calculations. Can either be a regular FlorisInterface object or can be an UncertaintyInterface object. + max_workers (int): Number of parallel workers, typically equal to the number of cores + you have on your system or HPC. + n_wind_direction_splits (int): Number of sectors to split the wind direction array over. + This is typically equal to max_workers, or a multiple of it. + n_wind_speed_splits (int): Number of sectors to split the wind speed array over. This is + typically 1 or 2. Defaults to 1. + interface (str): Parallel computing interface to leverage. Recommended is 'concurrent' + or 'multiprocessing' for local (single-system) use, and 'mpi4py' for high performance + computing on multiple nodes. Defaults to 'multiprocessing'. + use_mpi4py (bool): Deprecated option to enable/disable the usage of 'mpi4py'. This option + has been superseded by 'interface'. + propagate_flowfield_from_workers (bool): By enabling this, the flow field from every + floris object (one for each worker) is exported, combined and sent back to the main + module. This is slow so unless it's needed, it's recommended to be disabled. Defaults + to False. + print_timings (bool): Print the computation time to the console. Defaults to False. """ - # Load the correct library - if use_mpi4py: + # Set defaults for backward compatibility + if use_mpi4py is not None: + warnings.warn( + "The option 'mpi4py' will be removed in a future version. " + "Please use the option 'interface'." + ) + if use_mpi4py: + interface = "mpi4py" + else: + interface = "multiprocessing" + + if interface == "mpi4py": import mpi4py.futures as mp self._PoolExecutor = mp.MPIPoolExecutor - else: + elif interface == "multiprocessing": import multiprocessing as mp self._PoolExecutor = mp.Pool if max_workers is None: max_workers = mp.cpu_count() + elif interface == "concurrent": + from concurrent.futures import ProcessPoolExecutor + self._PoolExecutor = ProcessPoolExecutor + else: + raise UserWarning( + f"Interface '{interface}' not recognized. " + "Please use 'concurrent', 'multiprocessing' or 'mpi4py'." + ) # Initialize floris object and copy common properties self.fi = fi.copy() @@ -114,7 +150,7 @@ def __init__( np.min([max_workers, self.n_wind_direction_splits * self.n_wind_speed_splits]) ) self.propagate_flowfield_from_workers = propagate_flowfield_from_workers - self.use_mpi4py = use_mpi4py + self.interface = interface self.print_timings = print_timings def copy(self): @@ -171,7 +207,7 @@ def reinitialize( max_workers=self._max_workers, n_wind_direction_splits=self._n_wind_direction_splits, n_wind_speed_splits=self._n_wind_speed_splits, - use_mpi4py=self.use_mpi4py, + interface=self.interface, propagate_flowfield_from_workers=self.propagate_flowfield_from_workers, print_timings=self.print_timings, ) @@ -294,7 +330,15 @@ def get_turbine_powers(self, yaw_angles=None): # Perform parallel calculation t1 = timerpc() with self._PoolExecutor(self.max_workers) as p: - out = p.starmap(_get_turbine_powers_serial, multiargs) + if (self.interface == "mpi4py") or (self.interface == "multiprocessing"): + out = p.starmap(_get_turbine_powers_serial, multiargs) + else: + out = p.map( + _get_turbine_powers_serial, + [j[0] for j in multiargs], + [j[1] for j in multiargs] + ) + # out = list(out) t_execution = timerpc() - t1 # Postprocessing: merge power production (and opt. flow field) from individual runs @@ -491,7 +535,23 @@ def optimize_yaw_angles( # Optimize yaw angles using parallel processing print("Optimizing yaw angles with {:d} workers.".format(self.max_workers)) with self._PoolExecutor(self.max_workers) as p: - df_opt_splits = p.starmap(_optimize_yaw_angles_serial, multiargs) + if (self.interface == "mpi4py") or (self.interface == "multiprocessing"): + df_opt_splits = p.starmap(_optimize_yaw_angles_serial, multiargs) + else: + df_opt_splits = p.map( + _optimize_yaw_angles_serial, + [j[0] for j in multiargs], + [j[1] for j in multiargs], + [j[2] for j in multiargs], + [j[3] for j in multiargs], + [j[4] for j in multiargs], + [j[5] for j in multiargs], + [j[6] for j in multiargs], + [j[7] for j in multiargs], + [j[8] for j in multiargs], + [j[9] for j in multiargs], + [j[10] for j in multiargs] + ) t2 = timerpc() # Combine all solutions from multiprocessing into single dataframe diff --git a/floris/tools/visualization.py b/floris/tools/visualization.py index 529e9f8da..aa4d83734 100644 --- a/floris/tools/visualization.py +++ b/floris/tools/visualization.py @@ -14,6 +14,7 @@ from __future__ import annotations import copy +import warnings from typing import Union import matplotlib as mpl @@ -40,9 +41,10 @@ def plot_turbines( yaw_angles, rotor_diameters, color: str | None = None, - wind_direction: float = 270.0, ): """ + This function is deprecated and will be removed in v3.5, use `plot_turbines_with_fi` instead. + Plot wind plant layout from turbine locations. Args: @@ -50,21 +52,20 @@ def plot_turbines( layout_x (np.array): Wind turbine locations (east-west). layout_y (np.array): Wind turbine locations (north-south). yaw_angles (np.array): Yaw angles of each wind turbine. - D (float): Wind turbine rotor diameter. - color (str): Pyplot color option to plot the turbines. - wind_direction (float): Wind direction (rotates farm) + rotor_diameters (np.array): Wind turbine rotor diameter. + color (str): pyplot color option to plot the turbines. """ + warnings.warn( + "The `plot_turbines` function is deprecated and will be removed in v3.5, " + "use `plot_turbines_with_fi` instead.", + DeprecationWarning, + stacklevel=2 # This prints the calling function and this function in the warning + ) + if color is None: color = "k" - # Rotate layout to inertial frame for plotting turbines relative to wind direction - coordinates_array = np.array([[x, y, 0.0] for x, y in list(zip(layout_x, layout_y))]) - layout_x, layout_y, _, _, _ = rotate_coordinates_rel_west( - np.array([wind_direction]), - coordinates_array - ) - - for x, y, yaw, d in zip(layout_x[0,0], layout_y[0,0], yaw_angles, rotor_diameters): + for x, y, yaw, d in zip(layout_x, layout_y, yaw_angles, rotor_diameters): R = d / 2.0 x_0 = x + np.sin(np.deg2rad(yaw)) * R x_1 = x - np.sin(np.deg2rad(yaw)) * R @@ -75,14 +76,16 @@ def plot_turbines( def plot_turbines_with_fi( fi: FlorisInterface, - ax=None, - color=None, - wd=None, - yaw_angles=None, + ax: plt.Axes = None, + color: str = None, + wd: np.ndarray = None, + yaw_angles: np.ndarray = None, ): """ - Wrapper function to plot turbines which extracts the data - from a FLORIS interface object + Plot the wind plant layout from turbine locations gotten from a FlorisInterface object. + Note that this function automatically uses the first wind direction and first wind speed. + Generally, it is most explicit to create a new FlorisInterface with only the single + wind condition that should be plotted. Args: fi (:py:class:`floris.tools.floris_interface.FlorisInterface`): FlorisInterface object. @@ -101,15 +104,17 @@ def plot_turbines_with_fi( # Rotate yaw angles to inertial frame for plotting turbines relative to wind direction yaw_angles = yaw_angles - wind_delta(np.array(wd)) - plot_turbines( - ax, - fi.layout_x, - fi.layout_y, - yaw_angles.flatten(), - fi.floris.farm.rotor_diameters.flatten(), - color=color, - wind_direction=fi.floris.flow_field.wind_directions[0], - ) + if color is None: + color = "k" + + rotor_diameters = fi.floris.farm.rotor_diameters.flatten() + for x, y, yaw, d in zip(fi.layout_x, fi.layout_y, yaw_angles[0,0], rotor_diameters): + R = d / 2.0 + x_0 = x + np.sin(np.deg2rad(yaw)) * R + x_1 = x - np.sin(np.deg2rad(yaw)) * R + y_0 = y - np.cos(np.deg2rad(yaw)) * R + y_1 = y + np.cos(np.deg2rad(yaw)) * R + ax.plot([x_0, x_1], [y_0, y_1], color=color) def add_turbine_id_labels(fi: FlorisInterface, ax: plt.Axes, **kwargs): @@ -146,7 +151,13 @@ def add_turbine_id_labels(fi: FlorisInterface, ax: plt.Axes, **kwargs): ) -def line_contour_cut_plane(cut_plane, ax=None, levels=None, colors=None, **kwargs): +def line_contour_cut_plane( + cut_plane, + ax=None, + levels=None, + colors=None, + label_contours=False, + **kwargs): """ Visualize a cut_plane as a line contour plot. @@ -159,6 +170,8 @@ def line_contour_cut_plane(cut_plane, ax=None, levels=None, colors=None, **kwarg Defaults to None. colors (list, optional): Strings of color specification info. Defaults to None. + label_contours (Boolean, optional): Flag to include a numerical contour labels + on the plot. Defaults to False. **kwargs: Additional parameters to pass to `ax.contour`. """ @@ -178,7 +191,8 @@ def line_contour_cut_plane(cut_plane, ax=None, levels=None, colors=None, **kwarg **kwargs, ) - ax.clabel(contours, contours.levels, inline=True, fontsize=10, colors="black") + if label_contours: + ax.clabel(contours, contours.levels, inline=True, fontsize=10, colors="black") # Make equal axis ax.set_aspect("equal") @@ -194,6 +208,7 @@ def visualize_cut_plane( levels=None, clevels=None, color_bar=False, + label_contours=False, title="", **kwargs ): @@ -219,6 +234,8 @@ def visualize_cut_plane( Defaults to None. color_bar (Boolean, optional): Flag to include a color bar on the plot. Defaults to False. + label_contours (Boolean, optional): Flag to include a numerical contour labels + on the plot. Defaults to False. title (str, optional): User-supplied title for the plot. Defaults to "". **kwargs: Additional parameters to pass to line contour plot. @@ -270,6 +287,7 @@ def visualize_cut_plane( ax=ax, levels=levels, colors="b", + label_contours=label_contours, linewidths=0.8, alpha=0.3, **kwargs @@ -302,6 +320,7 @@ def visualize_heterogeneous_cut_plane( levels=None, clevels=None, color_bar=False, + label_contours=False, title="", plot_het_bounds=True, **kwargs @@ -329,6 +348,8 @@ def visualize_heterogeneous_cut_plane( Defaults to None. color_bar (Boolean, optional): Flag to include a color bar on the plot. Defaults to False. + label_contours (Boolean, optional): Flag to include a numerical contour labels + on the plot. Defaults to False. title (str, optional): User-supplied title for the plot. Defaults to "". plot_het_bonds (boolean, optional): Flag to include the user-defined bounds of the heterogeneous wind speed area. Defaults to True. @@ -381,6 +402,7 @@ def visualize_heterogeneous_cut_plane( ax=ax, levels=levels, colors="b", + label_contours=label_contours, linewidths=0.8, alpha=0.3, **kwargs diff --git a/floris/tools/wind_rose.py b/floris/tools/wind_rose.py index 94951e381..6725af485 100644 --- a/floris/tools/wind_rose.py +++ b/floris/tools/wind_rose.py @@ -162,7 +162,7 @@ def resample_wind_speed(self, df, ws=np.arange(0, 26, 1.0)): df["ws"] = pd.cut(df.ws, ws_edges, labels=ws) # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"]).sum() + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() # Fill nans df = df.fillna(0) @@ -261,7 +261,7 @@ def resample_wind_direction(self, df, wd=np.arange(0, 360, 5.0)): df["wd"] = pd.cut(df.wd, wd_edges, labels=wd) # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"]).sum() + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() # Fill nans df = df.fillna(0) @@ -335,7 +335,7 @@ def resample_column(self, df, col, bins): df[col] = pd.cut(df[col], var_edges, labels=bins) # Regroup - df = df.groupby([c for c in df.columns if c != "freq_val"]).sum() + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() # Fill nans df = df.fillna(0) @@ -688,7 +688,7 @@ def make_wind_rose_from_user_data( # Now group up df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"]).sum() + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() df = df.reset_index() @@ -1140,7 +1140,7 @@ def import_from_wind_toolkit_hsds( # Now group up df["freq_val"] = 1.0 - df = df.groupby([c for c in df.columns if c != "freq_val"]).sum() + df = df.groupby([c for c in df.columns if c != "freq_val"], observed=False).sum() df["freq_val"] = df.freq_val.astype(float) / df.freq_val.sum() df = df.reset_index() diff --git a/floris/turbine_library/iea_15MW_floating_multi_dim_cp_ct.yaml b/floris/turbine_library/iea_15MW_floating_multi_dim_cp_ct.yaml new file mode 100644 index 000000000..ea8623eee --- /dev/null +++ b/floris/turbine_library/iea_15MW_floating_multi_dim_cp_ct.yaml @@ -0,0 +1,29 @@ +turbine_type: 'iea_15MW_floating' +generator_efficiency: 1.0 +hub_height: 150.0 +pP: 1.88 +pT: 1.88 +rotor_diameter: 242.24 +TSR: 8.0 +ref_density_cp_ct: 1.225 +ref_tilt_cp_ct: 6.0 +multi_dimensional_cp_ct: True +power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv' +floating_tilt_table: + tilt: + - 5.747296314800103 + - 7.2342400188651068 + - 9.0468701999352397 + - 9.762182013267733 + - 8.795649572299896 + - 8.089078308325314 + - 7.7229584934943614 + wind_speeds: + - 4.0 + - 6.0 + - 8.0 + - 10.0 + - 12.0 + - 14.0 + - 16.0 +floating_correct_cp_ct_for_tilt: True diff --git a/floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv b/floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv new file mode 100644 index 000000000..b30eac5a3 --- /dev/null +++ b/floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv @@ -0,0 +1,213 @@ +Tp,Hs,ws,Cp,Ct +2,1,0,0,0 +2,1,3,0.049361236,0.817533319 +2,1,3.54953237,0.224324252,0.792115292 +2,1,4.067900771,0.312216418,0.786401899 +2,1,4.553906848,0.36009987,0.788898744 +2,1,5.006427063,0.38761204,0.790774576 +2,1,5.424415288,0.404010164,0.79208669 +2,1,5.806905228,0.413979324,0.79185809 +2,1,6.153012649,0.420083692,0.7903853 +2,1,6.461937428,0.423787764,0.788253035 +2,1,6.732965398,0.425977895,0.785845184 +2,1,6.965470002,0.427193272,0.783367164 +2,1,7.158913742,0.427183505,0.77853469 +2,1,7.312849418,0.426860928,0.77853469 +2,1,7.426921164,0.426617959,0.77853469 +2,1,7.500865272,0.426458783,0.77853469 +2,1,7.534510799,0.426385957,0.77853469 +2,1,7.541241633,0.426371389,0.77853469 +2,1,7.58833327,0.426268826,0.77853469 +2,1,7.675676842,0.426077456,0.77853469 +2,1,7.803070431,0.425795302,0.77853469 +2,1,7.970219531,0.425420049,0.77853469 +2,1,8.176737731,0.424948854,0.77853469 +2,1,8.422147605,0.424379028,0.77853469 +2,1,8.70588182,0.423707714,0.77853469 +2,1,9.027284445,0.422932811,0.77853469 +2,1,9.385612468,0.422052556,0.77853469 +2,1,9.780037514,0.421065815,0.77853469 +2,1,10.20964776,0.419972455,0.77853469 +2,1,10.67345004,0.419400676,0.781531069 +2,1,10.86770694,0.418981957,0.758935311 +2,1,11.17037214,0.385839135,0.614478855 +2,1,11.6992653,0.335840083,0.498687801 +2,1,12.25890683,0.29191329,0.416354609 +2,1,12.84800295,0.253572514,0.351944846 +2,1,13.46519181,0.220278082,0.299832337 +2,1,14.10904661,0.191477908,0.256956606 +2,1,14.77807889,0.166631343,0.221322169 +2,1,15.470742,0.145236797,0.19150758 +2,1,16.18543466,0.126834289,0.166435523 +2,1,16.92050464,0.111011925,0.145263684 +2,1,17.67425264,0.097406118,0.127319849 +2,1,18.44493615,0.085699408,0.11206048 +2,1,19.23077353,0.075616912,0.099042189 +2,1,20.02994808,0.066922115,0.087901155 +2,1,20.8406123,0.059412477,0.078337446 +2,1,21.66089211,0.052915227,0.07010295 +2,1,22.4888912,0.04728299,0.062991402 +2,1,23.32269542,0.042390922,0.056831647 +2,1,24.1603772,0.038132739,0.05148062 +2,1,25,0.03441828,0.046818787 +2,1,25.02,0,0 +2,1,50,0,0 +2,5,0,0,0 +2,5,3,0.024680618,0.40876666 +2,5,3.54953237,0.112162126,0.396057646 +2,5,4.067900771,0.156108209,0.39320095 +2,5,4.553906848,0.180049935,0.394449372 +2,5,5.006427063,0.19380602,0.395387288 +2,5,5.424415288,0.202005082,0.396043345 +2,5,5.806905228,0.206989662,0.395929045 +2,5,6.153012649,0.210041846,0.39519265 +2,5,6.461937428,0.211893882,0.394126518 +2,5,6.732965398,0.212988948,0.392922592 +2,5,6.965470002,0.213596636,0.391683582 +2,5,7.158913742,0.213591753,0.389267345 +2,5,7.312849418,0.213430464,0.389267345 +2,5,7.426921164,0.21330898,0.389267345 +2,5,7.500865272,0.213229392,0.389267345 +2,5,7.534510799,0.213192979,0.389267345 +2,5,7.541241633,0.213185695,0.389267345 +2,5,7.58833327,0.213134413,0.389267345 +2,5,7.675676842,0.213038728,0.389267345 +2,5,7.803070431,0.212897651,0.389267345 +2,5,7.970219531,0.212710025,0.389267345 +2,5,8.176737731,0.212474427,0.389267345 +2,5,8.422147605,0.212189514,0.389267345 +2,5,8.70588182,0.211853857,0.389267345 +2,5,9.027284445,0.211466406,0.389267345 +2,5,9.385612468,0.211026278,0.389267345 +2,5,9.780037514,0.210532908,0.389267345 +2,5,10.20964776,0.209986228,0.389267345 +2,5,10.67345004,0.209700338,0.390765535 +2,5,10.86770694,0.209490979,0.379467656 +2,5,11.17037214,0.192919568,0.307239428 +2,5,11.6992653,0.167920042,0.249343901 +2,5,12.25890683,0.145956645,0.208177305 +2,5,12.84800295,0.126786257,0.175972423 +2,5,13.46519181,0.110139041,0.149916169 +2,5,14.10904661,0.095738954,0.128478303 +2,5,14.77807889,0.083315672,0.110661085 +2,5,15.470742,0.072618399,0.09575379 +2,5,16.18543466,0.063417145,0.083217762 +2,5,16.92050464,0.055505963,0.072631842 +2,5,17.67425264,0.048703059,0.063659925 +2,5,18.44493615,0.042849704,0.05603024 +2,5,19.23077353,0.037808456,0.049521095 +2,5,20.02994808,0.033461058,0.043950578 +2,5,20.8406123,0.029706239,0.039168723 +2,5,21.66089211,0.026457614,0.035051475 +2,5,22.4888912,0.023641495,0.031495701 +2,5,23.32269542,0.021195461,0.028415824 +2,5,24.1603772,0.01906637,0.02574031 +2,5,25,0.01720914,0.023409394 +2,5,25.02,0,0 +2,5,50,0,0 +4,1,0,0,0 +4,1,3,0.012340309,0.20438333 +4,1,3.54953237,0.056081063,0.198028823 +4,1,4.067900771,0.078054105,0.196600475 +4,1,4.553906848,0.090024968,0.197224686 +4,1,5.006427063,0.09690301,0.197693644 +4,1,5.424415288,0.101002541,0.198021673 +4,1,5.806905228,0.103494831,0.197964523 +4,1,6.153012649,0.105020923,0.197596325 +4,1,6.461937428,0.105946941,0.197063259 +4,1,6.732965398,0.106494474,0.196461296 +4,1,6.965470002,0.106798318,0.195841791 +4,1,7.158913742,0.106795876,0.194633673 +4,1,7.312849418,0.106715232,0.194633673 +4,1,7.426921164,0.10665449,0.194633673 +4,1,7.500865272,0.106614696,0.194633673 +4,1,7.534510799,0.106596489,0.194633673 +4,1,7.541241633,0.106592847,0.194633673 +4,1,7.58833327,0.106567207,0.194633673 +4,1,7.675676842,0.106519364,0.194633673 +4,1,7.803070431,0.106448826,0.194633673 +4,1,7.970219531,0.106355012,0.194633673 +4,1,8.176737731,0.106237214,0.194633673 +4,1,8.422147605,0.106094757,0.194633673 +4,1,8.70588182,0.105926929,0.194633673 +4,1,9.027284445,0.105733203,0.194633673 +4,1,9.385612468,0.105513139,0.194633673 +4,1,9.780037514,0.105266454,0.194633673 +4,1,10.20964776,0.104993114,0.194633673 +4,1,10.67345004,0.104850169,0.195382767 +4,1,10.86770694,0.104745489,0.189733828 +4,1,11.17037214,0.096459784,0.153619714 +4,1,11.6992653,0.083960021,0.12467195 +4,1,12.25890683,0.072978323,0.104088652 +4,1,12.84800295,0.063393129,0.087986212 +4,1,13.46519181,0.055069521,0.074958084 +4,1,14.10904661,0.047869477,0.064239152 +4,1,14.77807889,0.041657836,0.055330542 +4,1,15.470742,0.036309199,0.047876895 +4,1,16.18543466,0.031708572,0.041608881 +4,1,16.92050464,0.027752981,0.036315921 +4,1,17.67425264,0.02435153,0.031829962 +4,1,18.44493615,0.021424852,0.02801512 +4,1,19.23077353,0.018904228,0.024760547 +4,1,20.02994808,0.016730529,0.021975289 +4,1,20.8406123,0.014853119,0.019584362 +4,1,21.66089211,0.013228807,0.017525738 +4,1,22.4888912,0.011820748,0.015747851 +4,1,23.32269542,0.010597731,0.014207912 +4,1,24.1603772,0.009533185,0.012870155 +4,1,25,0.00860457,0.011704697 +4,1,25.02,0,0 +4,1,50,0,0 +4,5,0,0,0 +4,5,3,0.006170155,0.102191665 +4,5,3.54953237,0.028040532,0.099014412 +4,5,4.067900771,0.039027052,0.098300238 +4,5,4.553906848,0.045012484,0.098612343 +4,5,5.006427063,0.048451505,0.098846822 +4,5,5.424415288,0.050501271,0.099010836 +4,5,5.806905228,0.051747416,0.098982261 +4,5,6.153012649,0.052510462,0.098798163 +4,5,6.461937428,0.052973471,0.09853163 +4,5,6.732965398,0.053247237,0.098230648 +4,5,6.965470002,0.053399159,0.097920896 +4,5,7.158913742,0.053397938,0.097316836 +4,5,7.312849418,0.053357616,0.097316836 +4,5,7.426921164,0.053327245,0.097316836 +4,5,7.500865272,0.053307348,0.097316836 +4,5,7.534510799,0.053298245,0.097316836 +4,5,7.541241633,0.053296424,0.097316836 +4,5,7.58833327,0.053283603,0.097316836 +4,5,7.675676842,0.053259682,0.097316836 +4,5,7.803070431,0.053224413,0.097316836 +4,5,7.970219531,0.053177506,0.097316836 +4,5,8.176737731,0.053118607,0.097316836 +4,5,8.422147605,0.053047379,0.097316836 +4,5,8.70588182,0.052963464,0.097316836 +4,5,9.027284445,0.052866602,0.097316836 +4,5,9.385612468,0.05275657,0.097316836 +4,5,9.780037514,0.052633227,0.097316836 +4,5,10.20964776,0.052496557,0.097316836 +4,5,10.67345004,0.052425085,0.097691384 +4,5,10.86770694,0.052372745,0.094866914 +4,5,11.17037214,0.048229892,0.076809857 +4,5,11.6992653,0.041980011,0.062335975 +4,5,12.25890683,0.036489161,0.052044326 +4,5,12.84800295,0.031696564,0.043993106 +4,5,13.46519181,0.02753476,0.037479042 +4,5,14.10904661,0.023934739,0.032119576 +4,5,14.77807889,0.020828918,0.027665271 +4,5,15.470742,0.0181546,0.023938448 +4,5,16.18543466,0.015854286,0.020804441 +4,5,16.92050464,0.013876491,0.018157961 +4,5,17.67425264,0.012175765,0.015914981 +4,5,18.44493615,0.010712426,0.01400756 +4,5,19.23077353,0.009452114,0.012380274 +4,5,20.02994808,0.008365265,0.010987645 +4,5,20.8406123,0.00742656,0.009792181 +4,5,21.66089211,0.006614404,0.008762869 +4,5,22.4888912,0.005910374,0.007873925 +4,5,23.32269542,0.005298865,0.007103956 +4,5,24.1603772,0.004766593,0.006435078 +4,5,25,0.004302285,0.005852349 +4,5,25.02,0,0 +4,5,50,0,0 diff --git a/floris/turbine_library/iea_15MW_multi_dim_cp_ct.yaml b/floris/turbine_library/iea_15MW_multi_dim_cp_ct.yaml new file mode 100644 index 000000000..51e0a83f6 --- /dev/null +++ b/floris/turbine_library/iea_15MW_multi_dim_cp_ct.yaml @@ -0,0 +1,11 @@ +turbine_type: 'iea_15MW_multi_dim_cp_ct' +generator_efficiency: 1.0 +hub_height: 150.0 +pP: 1.88 +pT: 1.88 +rotor_diameter: 242.24 +TSR: 8.0 +ref_density_cp_ct: 1.225 +ref_tilt_cp_ct: 6.0 +multi_dimensional_cp_ct: True +power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv' diff --git a/floris/turbine_library/nrel_5MW.yaml b/floris/turbine_library/nrel_5MW.yaml index 70c34a9f4..653ef14c7 100644 --- a/floris/turbine_library/nrel_5MW.yaml +++ b/floris/turbine_library/nrel_5MW.yaml @@ -197,3 +197,16 @@ power_thrust_table: - 25.01 - 25.02 - 50.0 + +### +# A boolean flag used when the user wants FLORIS to use the user-supplied multi-dimensional +# Cp/Ct information. +multi_dimensional_cp_ct: False + +### +# The path to the .csv file that contains the multi-dimensional Cp/Ct data. The format of this +# file is such that any external conditions, such as wave height or wave period, that the +# Cp/Ct data is dependent on come first, in column format. The last three columns of the .csv +# file must be ``ws``, ``Cp``, and ``Ct``, in that order. An example of fictional data is given +# in ``floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv``. +power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv' diff --git a/floris/version.py b/floris/version.py index 47b322c97..5a958026d 100644 --- a/floris/version.py +++ b/floris/version.py @@ -1 +1 @@ -3.4.1 +3.5 diff --git a/pyproject.toml b/pyproject.toml index 39683f439..2bb5fdcf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,8 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.per-file-ignores] # F841 unused-variable: ignore since this file uses numexpr and many variables look unused "floris/simulation/wake_deflection/jimenez.py" = ["F841"] +"floris/simulation/wake_turbulence/crespo_hernandez.py" = ["F841"] +"floris/simulation/wake_deflection/gauss.py" = ["F841"] "floris/simulation/wake_velocity/jensen.py" = ["F841"] "floris/simulation/wake_velocity/gauss.py" = ["F841"] "floris/simulation/wake_velocity/empirical_gauss.py" = ["F841"] diff --git a/setup.py b/setup.py index d3af38d21..0bab76eb1 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ # utilities "coloredlogs>=10.0", + "flatten_dict", ] # What packages are optional? diff --git a/tests/conftest.py b/tests/conftest.py index efa4fd13c..ab04fbde3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -361,6 +361,11 @@ def __init__(self): } self.turbine_floating["floating_correct_cp_ct_for_tilt"] = True + self.turbine_multi_dim = copy.deepcopy(self.turbine) + del self.turbine_multi_dim['power_thrust_table'] + self.turbine_multi_dim["multi_dimensional_cp_ct"] = True + self.turbine_multi_dim["power_thrust_data_file"] = "" + self.farm = { "layout_x": X_COORDS, "layout_y": Y_COORDS, @@ -402,7 +407,7 @@ def __init__(self): "empirical_gauss": { "horizontal_deflection_gain_D": 3.0, "vertical_deflection_gain_D": -1, - "deflection_rate": 15, + "deflection_rate": 30, "mixing_gain_deflection": 0.0, "yaw_added_mixing_gain": 0.0 }, @@ -432,7 +437,7 @@ def __init__(self): "sigma_max_rel": 4.0 }, "empirical_gauss": { - "wake_expansion_rates": [0.01, 0.005], + "wake_expansion_rates": [0.023, 0.008], "breakpoints_D": [10], "sigma_0_D": 0.28, "smoothing_length_D": 2.0, diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index 4c207ddeb..4dc28ef2e 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -42,26 +42,26 @@ # 8 m/s [ [7.9736330, 0.7636044, 1691326.6483808, 0.2568973], - [5.1827276, 0.8807411, 441118.3637433, 0.3273306], - [4.9925898, 0.8926413, 385869.8808447, 0.3361718], + [5.8890878, 0.8410986, 668931.9953790, 0.3006878], + [5.9448342, 0.8382459, 688269.8273350, 0.2989067], ], # 9m/s [ [8.9703371, 0.7625570, 2407841.6718785, 0.2563594], - [5.8355012, 0.8438407, 650343.4078478, 0.3024150], - [5.6871296, 0.8514332, 598874.9374620, 0.3072782], + [6.6288143, 0.8071935, 969952.7378773, 0.2804513], + [6.7440713, 0.8025559, 1023598.6805729, 0.2778266], ], # 10 m/s [ [9.9670412, 0.7529384, 3298067.1555604, 0.2514735], - [6.5341306, 0.8110034, 925882.5592972, 0.2826313], - [6.4005794, 0.8169593, 869713.2904634, 0.2860837], + [7.4019251, 0.7790665, 1355562.9527211, 0.2649822], + [7.5493339, 0.7745724, 1437063.0620195, 0.2626039], ], # 11 m/s [ [10.9637454, 0.7306256, 4363191.9880631, 0.2404936], - [7.3150380, 0.7819182, 1309551.0796815, 0.2665039], - [7.1452486, 0.7874908, 1219637.5477980, 0.2695064], + [8.2349756, 0.7622827, 1867008.5657835, 0.2562187], + [8.3523516, 0.7619629, 1946873.1634864, 0.2560548], ], ] ) @@ -71,26 +71,26 @@ # 8 m/s [ [7.9736330, 0.7606986, 1679924.0721706, 0.2549029], - [5.2892493, 0.8741162, 472289.7835635, 0.3225995], - [5.0661805, 0.8879895, 407013.1948403, 0.3326601], + [5.9257102, 0.8392246, 681635.9273649, 0.2995159], + [5.9615388, 0.8373911, 694064.4542077, 0.2983761], ], # 9 m/s [ [8.9703371, 0.7596552, 2391434.0080674, 0.2543734], - [5.9548519, 0.8377333, 691744.8624111, 0.2985883], - [5.7711008, 0.8471363, 628003.5991427, 0.3045110], + [6.6698959, 0.8055405, 989074.0018995, 0.2795122], + [6.7631531, 0.8017881, 1032480.2286024, 0.2773950], ], # 10 m/s [ [9.9670412, 0.7500732, 3275671.6727516, 0.2495630], - [6.6618693, 0.8058635, 985338.0488503, 0.2796954], - [6.4905463, 0.8128125, 906166.1389747, 0.2836741], + [7.4463751, 0.7776077, 1379101.8806016, 0.2642075], + [7.5701211, 0.7740351, 1449519.8581580, 0.2623212], ], # 11 m/s [ [10.9637454, 0.7278454, 4333842.6695283, 0.2387424], - [7.4437653, 0.7776933, 1377719.8294419, 0.2642530], - [7.2350472, 0.7845435, 1267191.1878400, 0.2679136], + [8.2809317, 0.7621575, 1898277.8462234, 0.2561545], + [8.3710828, 0.7619119, 1959618.1795131, 0.2560286], ], ] ) diff --git a/tests/turbine_multi_dim_unit_test.py b/tests/turbine_multi_dim_unit_test.py new file mode 100644 index 000000000..fd6cdacce --- /dev/null +++ b/tests/turbine_multi_dim_unit_test.py @@ -0,0 +1,318 @@ +# Copyright 2023 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +from scipy.interpolate import interp1d + +from floris.simulation import ( + Turbine, + TurbineMultiDimensional, +) +from floris.simulation.turbine_multi_dim import ( + axial_induction_multidim, + Ct_multidim, + multidim_Ct_down_select, + multidim_power_down_select, + MultiDimensionalPowerThrustTable, + power_multidim, +) +from tests.conftest import SampleInputs, WIND_SPEEDS + + +TEST_DATA = Path(__file__).resolve().parent.parent / "floris" / "turbine_library" +CSV_INPUT = TEST_DATA / "iea_15MW_multi_dim_Tp_Hs.csv" + + +# size 3 x 4 x 1 x 1 x 1 +WIND_CONDITION_BROADCAST = np.stack( + ( + np.reshape(np.array(WIND_SPEEDS), (-1, 1, 1, 1)), # Wind direction 0 + np.reshape(np.array(WIND_SPEEDS), (-1, 1, 1, 1)), # Wind direction 1 + np.reshape(np.array(WIND_SPEEDS), (-1, 1, 1, 1)), # Wind direction 2 + ), + axis=0, +) +INDEX_FILTER = [0, 2] + + +def test_multidim_Ct_down_select(): + CONDITIONS = {'Tp': 2, 'Hs': 1} + + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + turbine_type_map = np.array([turbine.turbine_type]) + turbine_type_map = turbine_type_map[None, None, :] + + downselect_turbine_fCts = multidim_Ct_down_select([[[turbine.fCt_interp]]], CONDITIONS) + + assert downselect_turbine_fCts == turbine.fCt_interp[(2, 1)] + + +def test_multidim_power_down_select(): + CONDITIONS = {'Tp': 2, 'Hs': 1} + + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + turbine_type_map = np.array([turbine.turbine_type]) + turbine_type_map = turbine_type_map[None, None, :] + + downselect_power_interps = multidim_power_down_select([[[turbine.power_interp]]], CONDITIONS) + + assert downselect_power_interps == turbine.power_interp[(2, 1)] + + +def test_multi_dimensional_power_thrust_table(): + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + df_data = pd.read_csv(turbine_data["power_thrust_data_file"]) + flattened_dict = MultiDimensionalPowerThrustTable.from_dataframe(df_data) + flattened_dict_base = { + ('Tp', '2', 'Hs', '1'): [], + ('Tp', '2', 'Hs', '5'): [], + ('Tp', '4', 'Hs', '1'): [], + ('Tp', '4', 'Hs', '5'): [], + } + assert flattened_dict == flattened_dict_base + + # Test for initialization errors + for el in ("ws", "Cp", "Ct"): + df_data = pd.read_csv(turbine_data["power_thrust_data_file"]) + df = df_data.drop(el, axis=1) + with pytest.raises(ValueError): + MultiDimensionalPowerThrustTable.from_dataframe(df) + + +def test_turbine_init(): + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + assert turbine.rotor_diameter == turbine_data["rotor_diameter"] + assert turbine.hub_height == turbine_data["hub_height"] + assert turbine.pP == turbine_data["pP"] + assert turbine.pT == turbine_data["pT"] + assert turbine.generator_efficiency == turbine_data["generator_efficiency"] + + assert isinstance(turbine.power_thrust_data, dict) + assert isinstance(turbine.fCp_interp, interp1d) + assert isinstance(turbine.fCt_interp, dict) + assert isinstance(turbine.power_interp, dict) + assert turbine.rotor_radius == turbine_data["rotor_diameter"] / 2.0 + + +def test_ct(): + N_TURBINES = 4 + + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + turbine_type_map = np.array(N_TURBINES * [turbine.turbine_type]) + turbine_type_map = turbine_type_map[None, None, :] + + # Single turbine + # yaw angle / fCt are (n wind direction, n wind speed, n turbine) + wind_speed = 10.0 + thrust = Ct_multidim( + velocities=wind_speed * np.ones((1, 1, 1, 3, 3)), + yaw_angle=np.zeros((1, 1, 1)), + tilt_angle=np.ones((1, 1, 1)) * 5.0, + ref_tilt_cp_ct=np.ones((1, 1, 1)) * 5.0, + fCt=np.array([[[turbine.fCt_interp[(2, 1)]]]]), + tilt_interp=np.array([(turbine.turbine_type, None)]), + correct_cp_ct_for_tilt=np.array([[[False]]]), + turbine_type_map=turbine_type_map[:,:,0] + ) + + print(thrust) + np.testing.assert_allclose(thrust, np.array([[[0.77853469]]])) + + # Multiple turbines with index filter + # 4 turbines with 3 x 3 grid arrays + thrusts = Ct_multidim( + velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 3 x 4 x 4 x 3 x 3 + yaw_angle=np.zeros((1, 1, N_TURBINES)), + tilt_angle=np.ones((1, 1, N_TURBINES)) * 5.0, + ref_tilt_cp_ct=np.ones((1, 1, N_TURBINES)) * 5.0, + fCt=np.tile( + [turbine.fCt_interp[(2, 1)]], + ( + np.shape(WIND_CONDITION_BROADCAST)[0], + np.shape(WIND_CONDITION_BROADCAST)[1], + N_TURBINES, + ) + ), + tilt_interp=np.array([(turbine.turbine_type, None)]), + correct_cp_ct_for_tilt=np.array([[[False] * N_TURBINES]]), + turbine_type_map=turbine_type_map, + ix_filter=INDEX_FILTER, + ) + assert len(thrusts[0, 0]) == len(INDEX_FILTER) + + print(thrusts) + + thrusts_truth = [ + [ + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.6957943, 0.6957943 ], + ], + [ + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.6957943, 0.6957943 ], + ], + [ + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.77853469, 0.77853469], + [0.6957943, 0.6957943 ], + ], + ] + + np.testing.assert_allclose(thrusts, thrusts_truth) + + +def test_power(): + N_TURBINES = 4 + AIR_DENSITY = 1.225 + + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + turbine_type_map = np.array(N_TURBINES * [turbine.turbine_type]) + turbine_type_map = turbine_type_map[None, None, :] + + # Single turbine + wind_speed = 10.0 + p = power_multidim( + ref_density_cp_ct=AIR_DENSITY, + rotor_effective_velocities=wind_speed * np.ones((1, 1, 1, 3, 3)), + power_interp=np.array([[[turbine.power_interp[(2, 1)]]]]), + ) + + power_truth = [ + [ + [ + [ + [3215682.686486, 3215682.686486, 3215682.686486], + [3215682.686486, 3215682.686486, 3215682.686486], + [3215682.686486, 3215682.686486, 3215682.686486], + ] + ] + ] + ] + + np.testing.assert_allclose(p, power_truth ) + + # Multiple turbines with ix filter + rotor_effective_velocities = np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST + p = power_multidim( + ref_density_cp_ct=AIR_DENSITY, + rotor_effective_velocities=rotor_effective_velocities, + power_interp=np.tile( + [turbine.power_interp[(2, 1)]], + ( + np.shape(WIND_CONDITION_BROADCAST)[0], + np.shape(WIND_CONDITION_BROADCAST)[1], + N_TURBINES, + ) + ), + ix_filter=INDEX_FILTER, + ) + assert len(p[0, 0]) == len(INDEX_FILTER) + + unique_power = turbine.power_interp[(2, 1)]( + np.unique(rotor_effective_velocities) + ) * AIR_DENSITY + + power_truth = np.zeros_like(rotor_effective_velocities) + for i in range(3): + for j in range(4): + for k in range(4): + for m in range(3): + for n in range(3): + power_truth[i, j, k, m, n] = unique_power[j] + + np.testing.assert_allclose(p, power_truth[:, :, INDEX_FILTER[0]:INDEX_FILTER[1], :, :]) + + +def test_axial_induction(): + + N_TURBINES = 4 + + turbine_data = SampleInputs().turbine_multi_dim + turbine_data["power_thrust_data_file"] = CSV_INPUT + turbine = TurbineMultiDimensional.from_dict(turbine_data) + turbine_type_map = np.array(N_TURBINES * [turbine.turbine_type]) + turbine_type_map = turbine_type_map[None, None, :] + + baseline_ai = 0.2646995 + + # Single turbine + wind_speed = 10.0 + ai = axial_induction_multidim( + velocities=wind_speed * np.ones((1, 1, 1, 3, 3)), + yaw_angle=np.zeros((1, 1, 1)), + tilt_angle=np.ones((1, 1, 1)) * 5.0, + ref_tilt_cp_ct=np.ones((1, 1, 1)) * 5.0, + fCt=np.array([[[turbine.fCt_interp[(2, 1)]]]]), + tilt_interp=np.array([(turbine.turbine_type, None)]), + correct_cp_ct_for_tilt=np.array([[[False]]]), + turbine_type_map=turbine_type_map[0,0,0], + ) + np.testing.assert_allclose(ai, baseline_ai) + + # Multiple turbines with ix filter + ai = axial_induction_multidim( + velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 3 x 4 x 4 x 3 x 3 + yaw_angle=np.zeros((1, 1, N_TURBINES)), + tilt_angle=np.ones((1, 1, N_TURBINES)) * 5.0, + ref_tilt_cp_ct=np.ones((1, 1, N_TURBINES)) * 5.0, + fCt=np.tile( + [turbine.fCt_interp[(2, 1)]], + ( + np.shape(WIND_CONDITION_BROADCAST)[0], + np.shape(WIND_CONDITION_BROADCAST)[1], + N_TURBINES, + ) + ), + tilt_interp=np.array([(turbine.turbine_type, None)] * N_TURBINES), + correct_cp_ct_for_tilt=np.array([[[False] * N_TURBINES]]), + turbine_type_map=turbine_type_map, + ix_filter=INDEX_FILTER, + ) + + assert len(ai[0, 0]) == len(INDEX_FILTER) + + # Test the 10 m/s wind speed to use the same baseline as above + np.testing.assert_allclose(ai[0,2], baseline_ai) + + +def test_asdict(sample_inputs_fixture: SampleInputs): + + turbine = Turbine.from_dict(sample_inputs_fixture.turbine) + dict1 = turbine.as_dict() + + new_turb = Turbine.from_dict(dict1) + dict2 = new_turb.as_dict() + + assert dict1 == dict2