sensotwin.stress_selection.ui

  1import ipywidgets as widgets
  2from . import configuration
  3from . import style
  4from .. import global_style
  5from .. import store_connection
  6import matplotlib.pyplot as plt
  7import matplotlib.transforms as trans
  8import matplotlib.patches as patches
  9import math
 10from traitlets.utils.bunch import Bunch
 11
 12
 13class StressSelectionUIElements:
 14    """Contains all UI functionality of operational parameter configuration UI (Step 3).
 15
 16    In order to use, show the top level widget 'self.dashboard' in one cell of a notebook
 17    and the apply css styling calling 'self.apply_styling()' an another cell
 18    """
 19    WIND_PROFILE_FILE_PATH = "Inputs/wind_data.vtt"
 20    def __init__(self):
 21        """Initialize UI objects and associated UI functionality."""
 22        self.dashboard = widgets.Tab().add_class("global_tab_container")
 23        self.dashboard.children = [
 24            self.init_data_source_box(),
 25            self.init_graph_input_box()
 26        ]
 27        tab_titles = ['Input', 'Simulation']
 28        for i in range(len(tab_titles)):
 29            self.dashboard.set_title(i, tab_titles[i])
 30
 31    def init_data_source_box(self) -> widgets.Box:
 32        """Initialize first tab of dashboard, choosing data source."""
 33        local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline")
 34        use_local_data_button = widgets.Button(
 35            description='Use local database',
 36            disabled=False,
 37            tooltip='Use local database',
 38            icon='play',
 39        ).add_class("global_load_data_button").add_class("global_basic_button")
 40        use_local_data_button.on_click(self.load_local_data)
 41        
 42        remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline")
 43        self.remote_database_input = widgets.Text(
 44            value=None,
 45            placeholder="insert SPARQL url here",
 46            description="SPARQL Endpoint:",
 47            disabled=False   
 48        ).add_class("global_url_input").add_class("global_basic_input")
 49        use_remote_data_button = widgets.Button(
 50            description='Use remote database',
 51            disabled=False,
 52            tooltip='Use remote database',
 53            icon='play',
 54        ).add_class("global_load_data_button").add_class("global_basic_button")
 55        use_remote_data_button.on_click(self.load_remote_data)
 56        
 57        local_data_box = widgets.VBox([
 58            local_database_label,
 59            use_local_data_button
 60        ])
 61        remote_data_box = widgets.VBox([
 62            remote_database_label,
 63            self.remote_database_input,
 64            use_remote_data_button
 65        ])
 66        data_source_box = widgets.VBox([
 67            local_data_box,
 68            remote_data_box
 69        ]).add_class("global_data_tab_container")
 70
 71        return data_source_box
 72
 73    def init_graph_input_box(self) -> widgets.Box:
 74        """Initialize second tab of dashboard, configuring operational parameters."""
 75        with plt.ioff():
 76            fig, (self.ax, self.pitch_ax) = plt.subplots(1, 2, width_ratios=[3, 1], figsize=(11.0, 6))
 77        fig.canvas.toolbar_visible = False
 78        fig.canvas.header_visible = False
 79        fig.canvas.footer_visible = False
 80        fig.canvas.resizable = False
 81        fig.set_facecolor((0.0, 0.0, 0.0, 0.0))
 82        self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 83        self.pitch_ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 84
 85        manual_parameters_label = widgets.Label(value="Manual parameters:").add_class("global_headline")
 86        self.windspeed_slider = widgets.FloatSlider(
 87            description='Windspeed:',
 88            disabled=False,
 89            continuous_update=True,
 90            orientation='horizontal',
 91            readout=True,
 92            readout_format='.2f',
 93        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 94        self.windspeed_input = widgets.FloatText(
 95            description='Windspeed (m/s):',
 96            disabled=False,
 97        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 98        self.duration_input = widgets.FloatText(
 99            description='Duration (y):',
100            disabled=False,
101            value=10,
102        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
103        self.windspeed_slider.observe(self.on_windspeed_slider_change, names="value")
104        self.windspeed_input.observe(self.on_windspeed_input_change, names="value")
105        self.duration_input.observe(self.on_duration_input_change, names="value")
106
107        derived_parameters_label = widgets.Label(value="Derived parameters:").add_class("global_headline")
108        self.pitch_angle_display = widgets.FloatText(
109            value=None,
110            description='Pitch angle (°):',
111            disabled=True,
112        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
113        self.rotation_speed_display = widgets.FloatText(
114            value=None,
115            description='Rotation speed (rpm):',
116            disabled=True,
117        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
118        self.rotation_total_display = widgets.IntText(
119            value=None,
120            description='Total rotations:',
121            disabled=True,
122        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
123
124        input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline")
125        self.input_set_selection = widgets.Select(
126            options=['No data loaded'],
127            value='No data loaded',
128            rows=10,
129            description='Input sets:',
130            disabled=False
131        ).add_class("global_input_set_selection").add_class("global_basic_input")
132        display_input_set_button = widgets.Button(
133            description='Load Input Set',
134            disabled=False,
135            tooltip='Load Input Set',
136            icon='play',
137        ).add_class("global_basic_button")
138        display_input_set_button.on_click(self.display_input_set)
139
140        selection_box = widgets.VBox([
141            manual_parameters_label,
142            self.windspeed_slider,
143            self.windspeed_input,
144            self.duration_input,
145            derived_parameters_label, 
146            self.pitch_angle_display,
147            self.rotation_speed_display,
148            self.rotation_total_display
149        ]).add_class("wind_sel_selection_box")
150        self.input_set_box = widgets.VBox([
151            input_set_selection_label,
152            self.input_set_selection,
153            display_input_set_button,
154            selection_box
155        ])
156
157        self.save_input_set_button = widgets.Button(
158            description='Save selected wind speed and duration as new input set',
159            disabled=False,
160            tooltip='Save selected wind speed and duration as new input set',
161            icon='save',
162        ).add_class("global_save_input_set_button").add_class("global_basic_button")
163        self.save_input_set_button.on_click(self.save_new_input_set)
164        graph_input_box = widgets.VBox([
165            widgets.HBox([
166                self.input_set_box,
167                fig.canvas
168            ]).add_class("wind_sel_graph_tab_container"),
169            self.save_input_set_button
170        ])
171
172        return graph_input_box
173
174    def update_plot(self, x_value: float, y_value: float):
175        """Update crosshair on 2D plot and rotate wind turbine display.
176        
177        Args:
178            x_value: X value of crosshair, wind speed in m/s
179            y_value: Y value of crosshair, pitch angle in °
180        """
181        if self.x_line:
182            self.x_line.remove()
183            self.y_line.remove()
184        # set new placement lines on graph
185        self.y_line = self.ax.axhline(y=y_value, color='r', linestyle='dotted')
186        self.x_line = self.ax.axvline(x=x_value, color='r', linestyle='dotted')
187        # rotate pitch display
188        base = plt.gca().transData
189        rot = trans.Affine2D().rotate_deg(-y_value)
190        self.pitch_bar[0].set_transform(rot + base)
191
192    def init_wind_display(self, ui_element=None):
193        """Initialize 2D wind speed plot and plot for displaying pitch angle.
194
195        Args:
196            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
197        """
198        self.wind_data = configuration.WindSpeedDataSet(self.WIND_PROFILE_FILE_PATH)
199        x_data = []
200        y_data = []
201        for _, data in self.wind_data.get_data().iterrows():
202            x_data.append(data["windspeed_mpers"])
203            y_data.append(data["pitch_angle_deg"])
204        # create windspeed to pitch diagram
205        self.ax.plot(x_data, y_data)
206        self.ax.set_xlabel('Wind speed (m/s)')
207        self.ax.set_ylabel('Pitch (°)')
208        # create pitch angle display diagram
209        self.y_line = None
210        self.x_line = None
211        self.pitch_bar = None
212        pitch_x, pitch_y = self.get_pitch_angle_graph_definition()
213        self.pitch_bar = self.pitch_ax.plot(pitch_x, pitch_y, color='tab:blue')
214        circle = patches.Circle((0,0), radius=1, facecolor=(0, 0, 0, 0), edgecolor="black", linestyle="solid",linewidth=2)
215        self.pitch_ax.add_patch(circle)
216        self.pitch_ax.set_aspect(1, adjustable='box')
217        self.pitch_ax.axis('off')
218        # adjust values last, it triggers change events and thus re-renders of the graph
219        self.windspeed_slider.min = x_data[0]
220        self.windspeed_slider.max = x_data[-1]
221        self.windspeed_slider.step = round(x_data[1]-x_data[0], 5)
222        initial_speed_value = x_data[math.floor(len(x_data) / 2)]
223        self.windspeed_slider.value = initial_speed_value 
224
225    def on_windspeed_slider_change(self, ui_element=None):
226        """Update UI after windspeed slider has changed value.
227
228        Args:
229            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
230        """
231        self.windspeed_changed(self.windspeed_slider.value)
232
233    def on_windspeed_input_change(self, ui_element=None):
234        """Update UI after windspeed number input has changed value.
235
236        Args:
237            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
238        """
239        self.windspeed_changed(self.windspeed_input.value)
240
241    def on_duration_input_change(self, change: Bunch):
242        """Update UI after duration input has changed value."""
243        self.update_total_rotations(change["new"], self.rotation_speed_display.value)
244
245    def windspeed_changed(self, speed: float):
246        """Update UI with data for new displayed wind speed."""
247        rounded_speed = round(speed,5)
248        self.windspeed_slider.value = rounded_speed
249        self.windspeed_input.value = rounded_speed
250        data = self.wind_data.get_single_entry(rounded_speed)
251        self.rotation_speed_display.value = data["rpm"]
252        self.pitch_angle_display.value = data["pitch_angle_deg"]
253        self.update_total_rotations(self.duration_input.value, data["rpm"])
254        self.update_plot(speed, data["pitch_angle_deg"])
255
256    def update_total_rotations(self, duration: float, rpm: float):
257        """Recalculate total rotations and update corresponding widget.
258        
259        Args:
260            duration: Duration in years
261            rpm: Rotations per minute
262        """
263        self.rotation_total_display.value = math.floor(duration * rpm * 60 * 24 * 365)
264
265    def get_pitch_angle_graph_definition(self)-> tuple:
266        """Return static point display used to draw wind turbine blade in matplot.
267        
268        Returns:
269            list of floats as X data, list of floats a Y data
270        """
271        x_data = [0.98, 0.92, 0.86, 0.8, 0.65, 0.5, 0.35, 0.2, 0.05, -0.1, -0.16, -0.22, -0.28, -0.34,
272                  -0.4, -0.46, -0.52, -0.58, -0.64, -0.7, -0.76, -0.81, -0.84, -0.87, -0.9, -0.93, -0.96,
273                  -0.975, -0.984, -0.99, -0.996, -0.9975, -0.999, -0.9996, -0.9998, -0.9992, -0.998,
274                  -0.9965, -0.992, -0.986, -0.98, -0.965, -0.94, -0.91, -0.88, -0.85, -0.82, -0.78, -0.72,
275                  -0.66, -0.6, -0.54, -0.48, -0.42, -0.36, -0.3, -0.24, -0.18, -0.12, 0, 0.15, 0.3, 0.45,
276                  0.6, 0.75, 0.84, 0.9, 0.96]
277        y_data = [0.00676, 0.02272, 0.03808, 0.05346, 0.092, 0.12932, 0.16354, 0.1932, 0.2166, 0.23198,
278                  0.23534, 0.23652, 0.23522, 0.23188, 0.22672, 0.21972, 0.2109, 0.20016, 0.18736, 0.17222,
279                  0.15438, 0.13698, 0.12516, 0.11206, 0.0973, 0.0802, 0.05938, 0.0464, 0.0369, 0.02914,
280                  0.01846, 0.01462, 0.00934, 0.00594, -0.00418, -0.00834, -0.01314, -0.0173, -0.02546,
281                  -0.03234, -0.0375, -0.04704, -0.0582, -0.06812, -0.07608, -0.08276, -0.08854, -0.09516,
282                  -0.10338, -0.10998, -0.11524, -0.11932, -0.12232, -0.12428, -0.12516, -0.12492, -0.12344,
283                  -0.12028, -0.11524, -0.1017, -0.08064, -0.05716, -0.0336, -0.01224, 0.00372, 0.00878,
284                  0.00922, 0.00608]
285        return x_data, y_data
286
287    def load_local_data(self, ui_element=None):
288        """Use locally stored data for UI.
289
290        Args:
291            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
292        """
293        self.conn = store_connection.LocalStoreConnection("sensotwin_world")
294        self.load_input_set_data()
295        self.init_wind_display()
296
297    def load_remote_data(self, ui_element=None):
298        """Use remotely stored data for UI.
299
300        Args:
301            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
302        """
303        self.conn = store_connection.FusekiConnection(self.remote_database_input.value)
304        self.load_input_set_data()
305        self.init_wind_display()
306
307    def save_new_input_set(self, ui_element=None):
308        """Save currently set data for wind profile and duration of simulation.
309
310        Args:
311            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
312        """
313        new_id = max(self.input_set_data.keys()) + 1 if self.input_set_data else 1
314        new_set = configuration.StressStrainInputDataSet(
315            uniq_id = new_id,
316            wind_speed = self.windspeed_input.value,
317            rotation_speed = self.rotation_speed_display.value,
318            pitch_angle = self.pitch_angle_display.value,
319            total_rotations = self.rotation_total_display.value,
320            duration = self.duration_input.value,
321            conn = self.conn)
322        new_set.save_entry_to_store()
323        self.load_input_set_data()
324
325    def load_input_set_data(self):
326        """Load data from previously set input source and populate UI."""
327        self.input_set_data = configuration.StressStrainInputDataSet.get_all_entries_from_store(self.conn)
328        if self.input_set_data:
329            options = []
330            for key, input_set in self.input_set_data.items():
331                label = input_set.generate_input_set_display_label()
332                options.append((label, key))
333            self.input_set_selection.options = options
334            self.input_set_selection.value = list(self.input_set_data.keys())[0]
335
336    def display_input_set(self, ui_element=None):
337        """Display data for currently selected input set in UI.
338
339        Args:
340            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
341        """
342        displayed_set = self.input_set_data[self.input_set_selection.value]
343
344        self.windspeed_input.value = displayed_set.get_wind_speed()
345        self.rotation_speed_display.value = displayed_set.get_rotation_speed()
346        self.pitch_angle_display.value = displayed_set.get_pitch_angle()
347        self.rotation_total_display.value = displayed_set.get_total_rotations()
348        self.duration_input.value = displayed_set.get_duration()
349
350        self.windspeed_changed(displayed_set.get_wind_speed())
351
352    def apply_styling(self):
353        css = """
354            <style>
355            {}
356            {}
357            </style>
358        """.format(global_style.global_css, style.local_css)
359        return widgets.HTML(css)
class StressSelectionUIElements:
 14class StressSelectionUIElements:
 15    """Contains all UI functionality of operational parameter configuration UI (Step 3).
 16
 17    In order to use, show the top level widget 'self.dashboard' in one cell of a notebook
 18    and the apply css styling calling 'self.apply_styling()' an another cell
 19    """
 20    WIND_PROFILE_FILE_PATH = "Inputs/wind_data.vtt"
 21    def __init__(self):
 22        """Initialize UI objects and associated UI functionality."""
 23        self.dashboard = widgets.Tab().add_class("global_tab_container")
 24        self.dashboard.children = [
 25            self.init_data_source_box(),
 26            self.init_graph_input_box()
 27        ]
 28        tab_titles = ['Input', 'Simulation']
 29        for i in range(len(tab_titles)):
 30            self.dashboard.set_title(i, tab_titles[i])
 31
 32    def init_data_source_box(self) -> widgets.Box:
 33        """Initialize first tab of dashboard, choosing data source."""
 34        local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline")
 35        use_local_data_button = widgets.Button(
 36            description='Use local database',
 37            disabled=False,
 38            tooltip='Use local database',
 39            icon='play',
 40        ).add_class("global_load_data_button").add_class("global_basic_button")
 41        use_local_data_button.on_click(self.load_local_data)
 42        
 43        remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline")
 44        self.remote_database_input = widgets.Text(
 45            value=None,
 46            placeholder="insert SPARQL url here",
 47            description="SPARQL Endpoint:",
 48            disabled=False   
 49        ).add_class("global_url_input").add_class("global_basic_input")
 50        use_remote_data_button = widgets.Button(
 51            description='Use remote database',
 52            disabled=False,
 53            tooltip='Use remote database',
 54            icon='play',
 55        ).add_class("global_load_data_button").add_class("global_basic_button")
 56        use_remote_data_button.on_click(self.load_remote_data)
 57        
 58        local_data_box = widgets.VBox([
 59            local_database_label,
 60            use_local_data_button
 61        ])
 62        remote_data_box = widgets.VBox([
 63            remote_database_label,
 64            self.remote_database_input,
 65            use_remote_data_button
 66        ])
 67        data_source_box = widgets.VBox([
 68            local_data_box,
 69            remote_data_box
 70        ]).add_class("global_data_tab_container")
 71
 72        return data_source_box
 73
 74    def init_graph_input_box(self) -> widgets.Box:
 75        """Initialize second tab of dashboard, configuring operational parameters."""
 76        with plt.ioff():
 77            fig, (self.ax, self.pitch_ax) = plt.subplots(1, 2, width_ratios=[3, 1], figsize=(11.0, 6))
 78        fig.canvas.toolbar_visible = False
 79        fig.canvas.header_visible = False
 80        fig.canvas.footer_visible = False
 81        fig.canvas.resizable = False
 82        fig.set_facecolor((0.0, 0.0, 0.0, 0.0))
 83        self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 84        self.pitch_ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 85
 86        manual_parameters_label = widgets.Label(value="Manual parameters:").add_class("global_headline")
 87        self.windspeed_slider = widgets.FloatSlider(
 88            description='Windspeed:',
 89            disabled=False,
 90            continuous_update=True,
 91            orientation='horizontal',
 92            readout=True,
 93            readout_format='.2f',
 94        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 95        self.windspeed_input = widgets.FloatText(
 96            description='Windspeed (m/s):',
 97            disabled=False,
 98        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 99        self.duration_input = widgets.FloatText(
100            description='Duration (y):',
101            disabled=False,
102            value=10,
103        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
104        self.windspeed_slider.observe(self.on_windspeed_slider_change, names="value")
105        self.windspeed_input.observe(self.on_windspeed_input_change, names="value")
106        self.duration_input.observe(self.on_duration_input_change, names="value")
107
108        derived_parameters_label = widgets.Label(value="Derived parameters:").add_class("global_headline")
109        self.pitch_angle_display = widgets.FloatText(
110            value=None,
111            description='Pitch angle (°):',
112            disabled=True,
113        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
114        self.rotation_speed_display = widgets.FloatText(
115            value=None,
116            description='Rotation speed (rpm):',
117            disabled=True,
118        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
119        self.rotation_total_display = widgets.IntText(
120            value=None,
121            description='Total rotations:',
122            disabled=True,
123        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
124
125        input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline")
126        self.input_set_selection = widgets.Select(
127            options=['No data loaded'],
128            value='No data loaded',
129            rows=10,
130            description='Input sets:',
131            disabled=False
132        ).add_class("global_input_set_selection").add_class("global_basic_input")
133        display_input_set_button = widgets.Button(
134            description='Load Input Set',
135            disabled=False,
136            tooltip='Load Input Set',
137            icon='play',
138        ).add_class("global_basic_button")
139        display_input_set_button.on_click(self.display_input_set)
140
141        selection_box = widgets.VBox([
142            manual_parameters_label,
143            self.windspeed_slider,
144            self.windspeed_input,
145            self.duration_input,
146            derived_parameters_label, 
147            self.pitch_angle_display,
148            self.rotation_speed_display,
149            self.rotation_total_display
150        ]).add_class("wind_sel_selection_box")
151        self.input_set_box = widgets.VBox([
152            input_set_selection_label,
153            self.input_set_selection,
154            display_input_set_button,
155            selection_box
156        ])
157
158        self.save_input_set_button = widgets.Button(
159            description='Save selected wind speed and duration as new input set',
160            disabled=False,
161            tooltip='Save selected wind speed and duration as new input set',
162            icon='save',
163        ).add_class("global_save_input_set_button").add_class("global_basic_button")
164        self.save_input_set_button.on_click(self.save_new_input_set)
165        graph_input_box = widgets.VBox([
166            widgets.HBox([
167                self.input_set_box,
168                fig.canvas
169            ]).add_class("wind_sel_graph_tab_container"),
170            self.save_input_set_button
171        ])
172
173        return graph_input_box
174
175    def update_plot(self, x_value: float, y_value: float):
176        """Update crosshair on 2D plot and rotate wind turbine display.
177        
178        Args:
179            x_value: X value of crosshair, wind speed in m/s
180            y_value: Y value of crosshair, pitch angle in °
181        """
182        if self.x_line:
183            self.x_line.remove()
184            self.y_line.remove()
185        # set new placement lines on graph
186        self.y_line = self.ax.axhline(y=y_value, color='r', linestyle='dotted')
187        self.x_line = self.ax.axvline(x=x_value, color='r', linestyle='dotted')
188        # rotate pitch display
189        base = plt.gca().transData
190        rot = trans.Affine2D().rotate_deg(-y_value)
191        self.pitch_bar[0].set_transform(rot + base)
192
193    def init_wind_display(self, ui_element=None):
194        """Initialize 2D wind speed plot and plot for displaying pitch angle.
195
196        Args:
197            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
198        """
199        self.wind_data = configuration.WindSpeedDataSet(self.WIND_PROFILE_FILE_PATH)
200        x_data = []
201        y_data = []
202        for _, data in self.wind_data.get_data().iterrows():
203            x_data.append(data["windspeed_mpers"])
204            y_data.append(data["pitch_angle_deg"])
205        # create windspeed to pitch diagram
206        self.ax.plot(x_data, y_data)
207        self.ax.set_xlabel('Wind speed (m/s)')
208        self.ax.set_ylabel('Pitch (°)')
209        # create pitch angle display diagram
210        self.y_line = None
211        self.x_line = None
212        self.pitch_bar = None
213        pitch_x, pitch_y = self.get_pitch_angle_graph_definition()
214        self.pitch_bar = self.pitch_ax.plot(pitch_x, pitch_y, color='tab:blue')
215        circle = patches.Circle((0,0), radius=1, facecolor=(0, 0, 0, 0), edgecolor="black", linestyle="solid",linewidth=2)
216        self.pitch_ax.add_patch(circle)
217        self.pitch_ax.set_aspect(1, adjustable='box')
218        self.pitch_ax.axis('off')
219        # adjust values last, it triggers change events and thus re-renders of the graph
220        self.windspeed_slider.min = x_data[0]
221        self.windspeed_slider.max = x_data[-1]
222        self.windspeed_slider.step = round(x_data[1]-x_data[0], 5)
223        initial_speed_value = x_data[math.floor(len(x_data) / 2)]
224        self.windspeed_slider.value = initial_speed_value 
225
226    def on_windspeed_slider_change(self, ui_element=None):
227        """Update UI after windspeed slider has changed value.
228
229        Args:
230            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
231        """
232        self.windspeed_changed(self.windspeed_slider.value)
233
234    def on_windspeed_input_change(self, ui_element=None):
235        """Update UI after windspeed number input has changed value.
236
237        Args:
238            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
239        """
240        self.windspeed_changed(self.windspeed_input.value)
241
242    def on_duration_input_change(self, change: Bunch):
243        """Update UI after duration input has changed value."""
244        self.update_total_rotations(change["new"], self.rotation_speed_display.value)
245
246    def windspeed_changed(self, speed: float):
247        """Update UI with data for new displayed wind speed."""
248        rounded_speed = round(speed,5)
249        self.windspeed_slider.value = rounded_speed
250        self.windspeed_input.value = rounded_speed
251        data = self.wind_data.get_single_entry(rounded_speed)
252        self.rotation_speed_display.value = data["rpm"]
253        self.pitch_angle_display.value = data["pitch_angle_deg"]
254        self.update_total_rotations(self.duration_input.value, data["rpm"])
255        self.update_plot(speed, data["pitch_angle_deg"])
256
257    def update_total_rotations(self, duration: float, rpm: float):
258        """Recalculate total rotations and update corresponding widget.
259        
260        Args:
261            duration: Duration in years
262            rpm: Rotations per minute
263        """
264        self.rotation_total_display.value = math.floor(duration * rpm * 60 * 24 * 365)
265
266    def get_pitch_angle_graph_definition(self)-> tuple:
267        """Return static point display used to draw wind turbine blade in matplot.
268        
269        Returns:
270            list of floats as X data, list of floats a Y data
271        """
272        x_data = [0.98, 0.92, 0.86, 0.8, 0.65, 0.5, 0.35, 0.2, 0.05, -0.1, -0.16, -0.22, -0.28, -0.34,
273                  -0.4, -0.46, -0.52, -0.58, -0.64, -0.7, -0.76, -0.81, -0.84, -0.87, -0.9, -0.93, -0.96,
274                  -0.975, -0.984, -0.99, -0.996, -0.9975, -0.999, -0.9996, -0.9998, -0.9992, -0.998,
275                  -0.9965, -0.992, -0.986, -0.98, -0.965, -0.94, -0.91, -0.88, -0.85, -0.82, -0.78, -0.72,
276                  -0.66, -0.6, -0.54, -0.48, -0.42, -0.36, -0.3, -0.24, -0.18, -0.12, 0, 0.15, 0.3, 0.45,
277                  0.6, 0.75, 0.84, 0.9, 0.96]
278        y_data = [0.00676, 0.02272, 0.03808, 0.05346, 0.092, 0.12932, 0.16354, 0.1932, 0.2166, 0.23198,
279                  0.23534, 0.23652, 0.23522, 0.23188, 0.22672, 0.21972, 0.2109, 0.20016, 0.18736, 0.17222,
280                  0.15438, 0.13698, 0.12516, 0.11206, 0.0973, 0.0802, 0.05938, 0.0464, 0.0369, 0.02914,
281                  0.01846, 0.01462, 0.00934, 0.00594, -0.00418, -0.00834, -0.01314, -0.0173, -0.02546,
282                  -0.03234, -0.0375, -0.04704, -0.0582, -0.06812, -0.07608, -0.08276, -0.08854, -0.09516,
283                  -0.10338, -0.10998, -0.11524, -0.11932, -0.12232, -0.12428, -0.12516, -0.12492, -0.12344,
284                  -0.12028, -0.11524, -0.1017, -0.08064, -0.05716, -0.0336, -0.01224, 0.00372, 0.00878,
285                  0.00922, 0.00608]
286        return x_data, y_data
287
288    def load_local_data(self, ui_element=None):
289        """Use locally stored data for UI.
290
291        Args:
292            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
293        """
294        self.conn = store_connection.LocalStoreConnection("sensotwin_world")
295        self.load_input_set_data()
296        self.init_wind_display()
297
298    def load_remote_data(self, ui_element=None):
299        """Use remotely stored data for UI.
300
301        Args:
302            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
303        """
304        self.conn = store_connection.FusekiConnection(self.remote_database_input.value)
305        self.load_input_set_data()
306        self.init_wind_display()
307
308    def save_new_input_set(self, ui_element=None):
309        """Save currently set data for wind profile and duration of simulation.
310
311        Args:
312            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
313        """
314        new_id = max(self.input_set_data.keys()) + 1 if self.input_set_data else 1
315        new_set = configuration.StressStrainInputDataSet(
316            uniq_id = new_id,
317            wind_speed = self.windspeed_input.value,
318            rotation_speed = self.rotation_speed_display.value,
319            pitch_angle = self.pitch_angle_display.value,
320            total_rotations = self.rotation_total_display.value,
321            duration = self.duration_input.value,
322            conn = self.conn)
323        new_set.save_entry_to_store()
324        self.load_input_set_data()
325
326    def load_input_set_data(self):
327        """Load data from previously set input source and populate UI."""
328        self.input_set_data = configuration.StressStrainInputDataSet.get_all_entries_from_store(self.conn)
329        if self.input_set_data:
330            options = []
331            for key, input_set in self.input_set_data.items():
332                label = input_set.generate_input_set_display_label()
333                options.append((label, key))
334            self.input_set_selection.options = options
335            self.input_set_selection.value = list(self.input_set_data.keys())[0]
336
337    def display_input_set(self, ui_element=None):
338        """Display data for currently selected input set in UI.
339
340        Args:
341            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
342        """
343        displayed_set = self.input_set_data[self.input_set_selection.value]
344
345        self.windspeed_input.value = displayed_set.get_wind_speed()
346        self.rotation_speed_display.value = displayed_set.get_rotation_speed()
347        self.pitch_angle_display.value = displayed_set.get_pitch_angle()
348        self.rotation_total_display.value = displayed_set.get_total_rotations()
349        self.duration_input.value = displayed_set.get_duration()
350
351        self.windspeed_changed(displayed_set.get_wind_speed())
352
353    def apply_styling(self):
354        css = """
355            <style>
356            {}
357            {}
358            </style>
359        """.format(global_style.global_css, style.local_css)
360        return widgets.HTML(css)

Contains all UI functionality of operational parameter configuration UI (Step 3).

In order to use, show the top level widget 'self.dashboard' in one cell of a notebook and the apply css styling calling 'self.apply_styling()' an another cell

StressSelectionUIElements()
21    def __init__(self):
22        """Initialize UI objects and associated UI functionality."""
23        self.dashboard = widgets.Tab().add_class("global_tab_container")
24        self.dashboard.children = [
25            self.init_data_source_box(),
26            self.init_graph_input_box()
27        ]
28        tab_titles = ['Input', 'Simulation']
29        for i in range(len(tab_titles)):
30            self.dashboard.set_title(i, tab_titles[i])

Initialize UI objects and associated UI functionality.

WIND_PROFILE_FILE_PATH = 'Inputs/wind_data.vtt'
dashboard
def init_data_source_box(self) -> ipywidgets.widgets.widget_box.Box:
32    def init_data_source_box(self) -> widgets.Box:
33        """Initialize first tab of dashboard, choosing data source."""
34        local_database_label = widgets.Label(value="Use local owlready2 database:").add_class("global_headline")
35        use_local_data_button = widgets.Button(
36            description='Use local database',
37            disabled=False,
38            tooltip='Use local database',
39            icon='play',
40        ).add_class("global_load_data_button").add_class("global_basic_button")
41        use_local_data_button.on_click(self.load_local_data)
42        
43        remote_database_label = widgets.Label(value="Use remote Apache Jena Fuseki database:").add_class("global_headline")
44        self.remote_database_input = widgets.Text(
45            value=None,
46            placeholder="insert SPARQL url here",
47            description="SPARQL Endpoint:",
48            disabled=False   
49        ).add_class("global_url_input").add_class("global_basic_input")
50        use_remote_data_button = widgets.Button(
51            description='Use remote database',
52            disabled=False,
53            tooltip='Use remote database',
54            icon='play',
55        ).add_class("global_load_data_button").add_class("global_basic_button")
56        use_remote_data_button.on_click(self.load_remote_data)
57        
58        local_data_box = widgets.VBox([
59            local_database_label,
60            use_local_data_button
61        ])
62        remote_data_box = widgets.VBox([
63            remote_database_label,
64            self.remote_database_input,
65            use_remote_data_button
66        ])
67        data_source_box = widgets.VBox([
68            local_data_box,
69            remote_data_box
70        ]).add_class("global_data_tab_container")
71
72        return data_source_box

Initialize first tab of dashboard, choosing data source.

def init_graph_input_box(self) -> ipywidgets.widgets.widget_box.Box:
 74    def init_graph_input_box(self) -> widgets.Box:
 75        """Initialize second tab of dashboard, configuring operational parameters."""
 76        with plt.ioff():
 77            fig, (self.ax, self.pitch_ax) = plt.subplots(1, 2, width_ratios=[3, 1], figsize=(11.0, 6))
 78        fig.canvas.toolbar_visible = False
 79        fig.canvas.header_visible = False
 80        fig.canvas.footer_visible = False
 81        fig.canvas.resizable = False
 82        fig.set_facecolor((0.0, 0.0, 0.0, 0.0))
 83        self.ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 84        self.pitch_ax.set_facecolor((0.0, 0.0, 0.0, 0.0))
 85
 86        manual_parameters_label = widgets.Label(value="Manual parameters:").add_class("global_headline")
 87        self.windspeed_slider = widgets.FloatSlider(
 88            description='Windspeed:',
 89            disabled=False,
 90            continuous_update=True,
 91            orientation='horizontal',
 92            readout=True,
 93            readout_format='.2f',
 94        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 95        self.windspeed_input = widgets.FloatText(
 96            description='Windspeed (m/s):',
 97            disabled=False,
 98        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
 99        self.duration_input = widgets.FloatText(
100            description='Duration (y):',
101            disabled=False,
102            value=10,
103        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
104        self.windspeed_slider.observe(self.on_windspeed_slider_change, names="value")
105        self.windspeed_input.observe(self.on_windspeed_input_change, names="value")
106        self.duration_input.observe(self.on_duration_input_change, names="value")
107
108        derived_parameters_label = widgets.Label(value="Derived parameters:").add_class("global_headline")
109        self.pitch_angle_display = widgets.FloatText(
110            value=None,
111            description='Pitch angle (°):',
112            disabled=True,
113        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
114        self.rotation_speed_display = widgets.FloatText(
115            value=None,
116            description='Rotation speed (rpm):',
117            disabled=True,
118        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
119        self.rotation_total_display = widgets.IntText(
120            value=None,
121            description='Total rotations:',
122            disabled=True,
123        ).add_class("global_basic_input").add_class("wind_sel_basic_input")
124
125        input_set_selection_label = widgets.Label(value="Select Input Set:").add_class("global_headline")
126        self.input_set_selection = widgets.Select(
127            options=['No data loaded'],
128            value='No data loaded',
129            rows=10,
130            description='Input sets:',
131            disabled=False
132        ).add_class("global_input_set_selection").add_class("global_basic_input")
133        display_input_set_button = widgets.Button(
134            description='Load Input Set',
135            disabled=False,
136            tooltip='Load Input Set',
137            icon='play',
138        ).add_class("global_basic_button")
139        display_input_set_button.on_click(self.display_input_set)
140
141        selection_box = widgets.VBox([
142            manual_parameters_label,
143            self.windspeed_slider,
144            self.windspeed_input,
145            self.duration_input,
146            derived_parameters_label, 
147            self.pitch_angle_display,
148            self.rotation_speed_display,
149            self.rotation_total_display
150        ]).add_class("wind_sel_selection_box")
151        self.input_set_box = widgets.VBox([
152            input_set_selection_label,
153            self.input_set_selection,
154            display_input_set_button,
155            selection_box
156        ])
157
158        self.save_input_set_button = widgets.Button(
159            description='Save selected wind speed and duration as new input set',
160            disabled=False,
161            tooltip='Save selected wind speed and duration as new input set',
162            icon='save',
163        ).add_class("global_save_input_set_button").add_class("global_basic_button")
164        self.save_input_set_button.on_click(self.save_new_input_set)
165        graph_input_box = widgets.VBox([
166            widgets.HBox([
167                self.input_set_box,
168                fig.canvas
169            ]).add_class("wind_sel_graph_tab_container"),
170            self.save_input_set_button
171        ])
172
173        return graph_input_box

Initialize second tab of dashboard, configuring operational parameters.

def update_plot(self, x_value: float, y_value: float):
175    def update_plot(self, x_value: float, y_value: float):
176        """Update crosshair on 2D plot and rotate wind turbine display.
177        
178        Args:
179            x_value: X value of crosshair, wind speed in m/s
180            y_value: Y value of crosshair, pitch angle in °
181        """
182        if self.x_line:
183            self.x_line.remove()
184            self.y_line.remove()
185        # set new placement lines on graph
186        self.y_line = self.ax.axhline(y=y_value, color='r', linestyle='dotted')
187        self.x_line = self.ax.axvline(x=x_value, color='r', linestyle='dotted')
188        # rotate pitch display
189        base = plt.gca().transData
190        rot = trans.Affine2D().rotate_deg(-y_value)
191        self.pitch_bar[0].set_transform(rot + base)

Update crosshair on 2D plot and rotate wind turbine display.

Args: x_value: X value of crosshair, wind speed in m/s y_value: Y value of crosshair, pitch angle in °

def init_wind_display(self, ui_element=None):
193    def init_wind_display(self, ui_element=None):
194        """Initialize 2D wind speed plot and plot for displaying pitch angle.
195
196        Args:
197            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
198        """
199        self.wind_data = configuration.WindSpeedDataSet(self.WIND_PROFILE_FILE_PATH)
200        x_data = []
201        y_data = []
202        for _, data in self.wind_data.get_data().iterrows():
203            x_data.append(data["windspeed_mpers"])
204            y_data.append(data["pitch_angle_deg"])
205        # create windspeed to pitch diagram
206        self.ax.plot(x_data, y_data)
207        self.ax.set_xlabel('Wind speed (m/s)')
208        self.ax.set_ylabel('Pitch (°)')
209        # create pitch angle display diagram
210        self.y_line = None
211        self.x_line = None
212        self.pitch_bar = None
213        pitch_x, pitch_y = self.get_pitch_angle_graph_definition()
214        self.pitch_bar = self.pitch_ax.plot(pitch_x, pitch_y, color='tab:blue')
215        circle = patches.Circle((0,0), radius=1, facecolor=(0, 0, 0, 0), edgecolor="black", linestyle="solid",linewidth=2)
216        self.pitch_ax.add_patch(circle)
217        self.pitch_ax.set_aspect(1, adjustable='box')
218        self.pitch_ax.axis('off')
219        # adjust values last, it triggers change events and thus re-renders of the graph
220        self.windspeed_slider.min = x_data[0]
221        self.windspeed_slider.max = x_data[-1]
222        self.windspeed_slider.step = round(x_data[1]-x_data[0], 5)
223        initial_speed_value = x_data[math.floor(len(x_data) / 2)]
224        self.windspeed_slider.value = initial_speed_value 

Initialize 2D wind speed plot and plot for displaying pitch angle.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def on_windspeed_slider_change(self, ui_element=None):
226    def on_windspeed_slider_change(self, ui_element=None):
227        """Update UI after windspeed slider has changed value.
228
229        Args:
230            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
231        """
232        self.windspeed_changed(self.windspeed_slider.value)

Update UI after windspeed slider has changed value.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def on_windspeed_input_change(self, ui_element=None):
234    def on_windspeed_input_change(self, ui_element=None):
235        """Update UI after windspeed number input has changed value.
236
237        Args:
238            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
239        """
240        self.windspeed_changed(self.windspeed_input.value)

Update UI after windspeed number input has changed value.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def on_duration_input_change(self, change: traitlets.utils.bunch.Bunch):
242    def on_duration_input_change(self, change: Bunch):
243        """Update UI after duration input has changed value."""
244        self.update_total_rotations(change["new"], self.rotation_speed_display.value)

Update UI after duration input has changed value.

def windspeed_changed(self, speed: float):
246    def windspeed_changed(self, speed: float):
247        """Update UI with data for new displayed wind speed."""
248        rounded_speed = round(speed,5)
249        self.windspeed_slider.value = rounded_speed
250        self.windspeed_input.value = rounded_speed
251        data = self.wind_data.get_single_entry(rounded_speed)
252        self.rotation_speed_display.value = data["rpm"]
253        self.pitch_angle_display.value = data["pitch_angle_deg"]
254        self.update_total_rotations(self.duration_input.value, data["rpm"])
255        self.update_plot(speed, data["pitch_angle_deg"])

Update UI with data for new displayed wind speed.

def update_total_rotations(self, duration: float, rpm: float):
257    def update_total_rotations(self, duration: float, rpm: float):
258        """Recalculate total rotations and update corresponding widget.
259        
260        Args:
261            duration: Duration in years
262            rpm: Rotations per minute
263        """
264        self.rotation_total_display.value = math.floor(duration * rpm * 60 * 24 * 365)

Recalculate total rotations and update corresponding widget.

Args: duration: Duration in years rpm: Rotations per minute

def get_pitch_angle_graph_definition(self) -> tuple:
266    def get_pitch_angle_graph_definition(self)-> tuple:
267        """Return static point display used to draw wind turbine blade in matplot.
268        
269        Returns:
270            list of floats as X data, list of floats a Y data
271        """
272        x_data = [0.98, 0.92, 0.86, 0.8, 0.65, 0.5, 0.35, 0.2, 0.05, -0.1, -0.16, -0.22, -0.28, -0.34,
273                  -0.4, -0.46, -0.52, -0.58, -0.64, -0.7, -0.76, -0.81, -0.84, -0.87, -0.9, -0.93, -0.96,
274                  -0.975, -0.984, -0.99, -0.996, -0.9975, -0.999, -0.9996, -0.9998, -0.9992, -0.998,
275                  -0.9965, -0.992, -0.986, -0.98, -0.965, -0.94, -0.91, -0.88, -0.85, -0.82, -0.78, -0.72,
276                  -0.66, -0.6, -0.54, -0.48, -0.42, -0.36, -0.3, -0.24, -0.18, -0.12, 0, 0.15, 0.3, 0.45,
277                  0.6, 0.75, 0.84, 0.9, 0.96]
278        y_data = [0.00676, 0.02272, 0.03808, 0.05346, 0.092, 0.12932, 0.16354, 0.1932, 0.2166, 0.23198,
279                  0.23534, 0.23652, 0.23522, 0.23188, 0.22672, 0.21972, 0.2109, 0.20016, 0.18736, 0.17222,
280                  0.15438, 0.13698, 0.12516, 0.11206, 0.0973, 0.0802, 0.05938, 0.0464, 0.0369, 0.02914,
281                  0.01846, 0.01462, 0.00934, 0.00594, -0.00418, -0.00834, -0.01314, -0.0173, -0.02546,
282                  -0.03234, -0.0375, -0.04704, -0.0582, -0.06812, -0.07608, -0.08276, -0.08854, -0.09516,
283                  -0.10338, -0.10998, -0.11524, -0.11932, -0.12232, -0.12428, -0.12516, -0.12492, -0.12344,
284                  -0.12028, -0.11524, -0.1017, -0.08064, -0.05716, -0.0336, -0.01224, 0.00372, 0.00878,
285                  0.00922, 0.00608]
286        return x_data, y_data

Return static point display used to draw wind turbine blade in matplot.

Returns: list of floats as X data, list of floats a Y data

def load_local_data(self, ui_element=None):
288    def load_local_data(self, ui_element=None):
289        """Use locally stored data for UI.
290
291        Args:
292            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
293        """
294        self.conn = store_connection.LocalStoreConnection("sensotwin_world")
295        self.load_input_set_data()
296        self.init_wind_display()

Use locally stored data for UI.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def load_remote_data(self, ui_element=None):
298    def load_remote_data(self, ui_element=None):
299        """Use remotely stored data for UI.
300
301        Args:
302            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
303        """
304        self.conn = store_connection.FusekiConnection(self.remote_database_input.value)
305        self.load_input_set_data()
306        self.init_wind_display()

Use remotely stored data for UI.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def save_new_input_set(self, ui_element=None):
308    def save_new_input_set(self, ui_element=None):
309        """Save currently set data for wind profile and duration of simulation.
310
311        Args:
312            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
313        """
314        new_id = max(self.input_set_data.keys()) + 1 if self.input_set_data else 1
315        new_set = configuration.StressStrainInputDataSet(
316            uniq_id = new_id,
317            wind_speed = self.windspeed_input.value,
318            rotation_speed = self.rotation_speed_display.value,
319            pitch_angle = self.pitch_angle_display.value,
320            total_rotations = self.rotation_total_display.value,
321            duration = self.duration_input.value,
322            conn = self.conn)
323        new_set.save_entry_to_store()
324        self.load_input_set_data()

Save currently set data for wind profile and duration of simulation.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def load_input_set_data(self):
326    def load_input_set_data(self):
327        """Load data from previously set input source and populate UI."""
328        self.input_set_data = configuration.StressStrainInputDataSet.get_all_entries_from_store(self.conn)
329        if self.input_set_data:
330            options = []
331            for key, input_set in self.input_set_data.items():
332                label = input_set.generate_input_set_display_label()
333                options.append((label, key))
334            self.input_set_selection.options = options
335            self.input_set_selection.value = list(self.input_set_data.keys())[0]

Load data from previously set input source and populate UI.

def display_input_set(self, ui_element=None):
337    def display_input_set(self, ui_element=None):
338        """Display data for currently selected input set in UI.
339
340        Args:
341            ui_element: Override for ipywidgets to not pass the UI element that triggered the event
342        """
343        displayed_set = self.input_set_data[self.input_set_selection.value]
344
345        self.windspeed_input.value = displayed_set.get_wind_speed()
346        self.rotation_speed_display.value = displayed_set.get_rotation_speed()
347        self.pitch_angle_display.value = displayed_set.get_pitch_angle()
348        self.rotation_total_display.value = displayed_set.get_total_rotations()
349        self.duration_input.value = displayed_set.get_duration()
350
351        self.windspeed_changed(displayed_set.get_wind_speed())

Display data for currently selected input set in UI.

Args: ui_element: Override for ipywidgets to not pass the UI element that triggered the event

def apply_styling(self):
353    def apply_styling(self):
354        css = """
355            <style>
356            {}
357            {}
358            </style>
359        """.format(global_style.global_css, style.local_css)
360        return widgets.HTML(css)